HMCL-dev/HMCL · error · JsonParseException

Theme-pack manifest must declare exactly one of theme or…

Error message

Theme-pack manifest must declare exactly one of theme or themes

What it means

Thrown by readThemes when the manifest declares neither or both of the mutually exclusive keys "theme" and "themes". Exactly one must be present: "theme" for a single-theme pack, "themes" for a multi-theme pack.

Solutions

  1. If the pack has one theme, keep only the "theme" key and delete "themes".
  2. If the pack has multiple themes, keep only the "themes" array and delete "theme".
  3. Fix the top-level key name — it must be exactly "theme" or "themes".

Example fix

// before
{ "theme": {...}, "themes": [{...}] }
// after
{ "themes": [{...}] }
Defensive patterns

Strategy: validation

Validate before calling

JsonObject manifest = Json.parse(text).asObject();
boolean hasTheme = manifest.get("theme") != null;
boolean hasThemes = manifest.get("themes") != null;
if (hasTheme == hasThemes) {
    throw new IllegalArgumentException("declare exactly one of 'theme' or 'themes'");
}

Prevention

When it happens

Trigger: Parsing a manifest JSON where object.has("theme") == object.has("themes") — both keys present, or both absent.

Common situations: Merging manifests by hand and leaving both keys; renaming "theme" to something else (e.g. "skin") so neither key matches; converting a single-theme manifest to multi-theme by adding "themes" without removing "theme".

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManifest.java:145

    public @Nullable Theme findTheme(@Nullable String themeId) {
        if (themeId == null) {
            return themes.size() == 1 ? themes.get(0) : null;
        }

        for (Theme theme : themes) {
            if (themeId.equals(theme.id())) {
                return theme;
            }
        }
        return null;
    }

    /// Reads the required theme declaration.
    private static List<Theme> readThemes(JsonObject object) {
        boolean hasSingleTheme = object.has("theme");
        boolean hasMultipleThemes = object.has("themes");
        if (hasSingleTheme == hasMultipleThemes) {
            throw new JsonParseException("Theme-pack manifest must declare exactly one of theme or themes");
        }

        if (hasSingleTheme) {
            JsonElement element = object.get("theme");
            if (!(element instanceof JsonObject themeObject)) {
                throw new JsonParseException("Theme-pack theme must be an object");
            }
            return List.of(Theme.fromJson(themeObject, false));
        }

        JsonElement element = object.get("themes");
        if (!(element instanceof JsonArray array)) {
            throw new JsonParseException("Theme-pack manifest is missing themes array");
        }
        if (array.isEmpty()) {
            throw new JsonParseException("Theme-pack themes array must declare at least one theme");
        }

View on GitHub (pinned to 24702dc5a0)