HMCL-dev/HMCL · error · JsonParseException

Game directory ID cannot be nil

Error message

Game directory ID cannot be nil

What it means

GameDirectory's deserializer rejects entries whose "id" decodes to the special GameDirectoryID.NIL sentinel (e.g. an all-zero/placeholder UUID). NIL is reserved for internal/default use, so a persisted directory with NIL has no meaningful identity and JsonParseException("Game directory ID cannot be nil") is thrown. Note the whole element not being an object yields null earlier — this error fires only for object entries with a NIL id.

Solutions

  1. Replace the nil/zero ID with a fresh unique ID (e.g. a random UUID) in settings.json
  2. Re-add the game directory through the HMCL UI, which generates a valid ID
  3. Find the code path that saved GameDirectoryID.NIL (uninitialized ID property) and ensure a real ID is assigned before persistence
  4. Delete the offending entry and let HMCL recreate it

Example fix

// before
{ "id": "00000000-0000-0000-0000-000000000000", "path": "D:/minecraft" }
// after
{ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "path": "D:/minecraft" }
Defensive patterns

Strategy: validation

Validate before calling

JsonObject entry = ...;
String id = entry.getAsJsonPrimitive("id").getAsString();
if (id.matches("^0{8}(-0{4}){3}-0{12}$"))
    throw new IOException("Game directory has nil id; assign a real unique id");

Type guard

static boolean hasRealId(JsonObject entry) {
    JsonElement id = entry.get("id");
    return id != null && id.isJsonPrimitive()
        && !"00000000-0000-0000-0000-000000000000".equals(id.getAsString());
}

Try / catch

try {
    GameDirectory d = gson.fromJson(entry, GameDirectory.class);
} catch (JsonParseException e) {
    if (e.getMessage().contains("nil")) {
        LOGGER.warning("Preset/directory had placeholder id; regenerating");
        // replace id with a fresh UUID and retry
    }
}

Prevention

When it happens

Trigger: Deserializing a settings JSON entry where the "id" field deserializes into GameDirectoryID.NIL — e.g. an id serialized as all zeros/empty placeholder because the ID was never initialized before saving.

Common situations: A tool or old build that wrote a default/zero UUID for new directories; config generated by templating that left the id as a placeholder; saving before the ID property was initialized due to a version bug.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/51b3ceaf8825eb43. Report an issue: GitHub.

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/setting/GameDirectory.java:195

                }
            }
            jsonObject.add("path", context.serialize(src.getPath(), PortablePath.class));
            if (src.getLegacyGameSettings() != null) {
                jsonObject.add("legacyGameSettings", context.serialize(src.getLegacyGameSettings(), GameSettingsPresetID.class));
            }

            return jsonObject;
        }

        /// Deserializes a game directory from JSON.
        @Override
        public @Nullable GameDirectory deserialize(@Nullable JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            if (!(json instanceof JsonObject obj)) return null;
            GameDirectoryID id = context.deserialize(obj.get("id"), GameDirectoryID.class);
            if (id == null) {
                throw new JsonParseException("Game directory ID cannot be null");
            } else if (GameDirectoryID.NIL.equals(id)) {
                throw new JsonParseException("Game directory ID cannot be nil");
            }
            PortablePath path = context.deserialize(obj.get("path"), PortablePath.class);
            if (path == null) {
                throw new JsonParseException("Game directory path cannot be null");
            }
            @Nullable LocalizedText name = context.deserialize(obj.get("name"), LocalizedText.class);

            return new GameDirectory(id,
                    name,
                    path,
                    context.deserialize(obj.get("legacyGameSettings"), GameSettingsPresetID.class));
        }

    }
}

View on GitHub (pinned to 24702dc5a0)