HMCL-dev/HMCL · error · JsonParseException
json.toString()
Error message
json.toString()
What it means
JavaManifest's Gson deserializer requires the JSON document to be a JSON object. If the top-level element is an array, string, number, or boolean, it throws JsonParseException whose message is the raw JSON (json.toString()). This fails fast before attempting to read the os.name/os.arch/java.version primitives.
Solutions
- Check the URL being fetched — ensure it points directly at the manifest JSON object, not a wrapper or error page
- Log/inspect json.toString() from the exception message to see what was actually parsed and fix the source data
- If the server wraps the manifest, deserialize the envelope first and pass the inner object to the JavaManifest adapter
- Add a retry/refetch for the manifest in case of transient download corruption
Example fix
// before
JavaManifest m = gson.fromJson(errorPageHtml, JavaManifest.class);
// after
String body = download(manifestUrl);
if (!body.trim().startsWith("{")) throw new IOException("Manifest is not JSON object: " + body.substring(0, 80));
JavaManifest m = gson.fromJson(body, JavaManifest.class); Defensive patterns
Strategy: validation
Validate before calling
String body = download(manifestUrl);
JsonElement el = JsonParser.parseString(body);
if (!el.isJsonObject()) throw new IOException("JavaManifest JSON is not an object"); Type guard
static boolean isManifestShape(JsonElement el) {
return el.isJsonObject()
&& el.getAsJsonObject().has("os.name")
&& el.getAsJsonObject().has("os.arch");
} Try / catch
try {
JavaManifest m = gson.fromJson(body, JavaManifest.class);
} catch (JsonParseException e) {
LOGGER.warning("Bad manifest payload: " + e.getMessage().substring(0, Math.min(200, e.getMessage().length())));
// refetch or fall back to cached manifest
} Prevention
- Check HTTP status and content-type before parsing the manifest body
- Sniff that the response starts with '{' before handing it to Gson
- Cache the last good manifest and fall back to it on parse failure
- Pin the manifest URL to the exact runtime version
When it happens
Trigger: Gson deserializing a JavaManifest when the fetched JSON (e.g. a Mojang/BabaFX runtime manifest response) is not a JsonObject — a wrapped response, an error page, an array of manifests, or a corrupt/truncated file.
Common situations: Downloaded runtime manifest is actually an HTML error page or CDN error JSON; API endpoint changed and now returns an envelope object with the manifest nested one level down; manually edited/corrupted manifest cache file.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- json.toString()
- e
- Account private data is not an object
- Game directory ID cannot be null
- Game directory ID cannot be nil
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/50b425dd70b64bbf.
Report an issue: GitHub.
Appendix: source
Thrown at HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManifest.java:63
JsonObject res = new JsonObject();
res.addProperty("os.name", src.info().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.info().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.info().getVersion());
res.addProperty("java.vendor", src.info().getVendor());
if (src.update() != null)
res.add("update", context.serialize(src.update()));
if (src.files() != null)
res.add("files", context.serialize(src.files(), LOCAL_FILES_TYPE));
return res;
}
@Override
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
try {
JsonObject jsonObject = json.getAsJsonObject();
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
if (osName == null || osArch == null || javaVersion == null)
throw new JsonParseException(json.toString());
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
} catch (JsonParseException e) {
throw e;
} catch (Throwable e) {View on GitHub (pinned to 24702dc5a0)