{"record":{"id":"3a4c037f818a0630","repo":"HMCL-dev/HMCL","slug":"theme-pack-manifest-must-declare-exactly-one-of-th","errorCode":null,"errorMessage":"Theme-pack manifest must declare exactly one of theme or themes","messagePattern":"Theme-pack manifest must declare exactly one of theme or themes","errorType":"validation","errorClass":"JsonParseException","httpStatus":null,"severity":"error","filePath":"HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManifest.java","lineNumber":145,"sourceCode":"    public @Nullable Theme findTheme(@Nullable String themeId) {\n        if (themeId == null) {\n            return themes.size() == 1 ? themes.get(0) : null;\n        }\n\n        for (Theme theme : themes) {\n            if (themeId.equals(theme.id())) {\n                return theme;\n            }\n        }\n        return null;\n    }\n\n    /// Reads the required theme declaration.\n    private static List<Theme> readThemes(JsonObject object) {\n        boolean hasSingleTheme = object.has(\"theme\");\n        boolean hasMultipleThemes = object.has(\"themes\");\n        if (hasSingleTheme == hasMultipleThemes) {\n            throw new JsonParseException(\"Theme-pack manifest must declare exactly one of theme or themes\");\n        }\n\n        if (hasSingleTheme) {\n            JsonElement element = object.get(\"theme\");\n            if (!(element instanceof JsonObject themeObject)) {\n                throw new JsonParseException(\"Theme-pack theme must be an object\");\n            }\n            return List.of(Theme.fromJson(themeObject, false));\n        }\n\n        JsonElement element = object.get(\"themes\");\n        if (!(element instanceof JsonArray array)) {\n            throw new JsonParseException(\"Theme-pack manifest is missing themes array\");\n        }\n        if (array.isEmpty()) {\n            throw new JsonParseException(\"Theme-pack themes array must declare at least one theme\");\n        }\n","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManifest.java#L127-L163","documentation":"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.","triggerScenarios":"Parsing a manifest JSON where object.has(\"theme\") == object.has(\"themes\") — both keys present, or both absent.","commonSituations":"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\".","solutions":["If the pack has one theme, keep only the \"theme\" key and delete \"themes\".","If the pack has multiple themes, keep only the \"themes\" array and delete \"theme\".","Fix the top-level key name — it must be exactly \"theme\" or \"themes\"."],"exampleFix":"// before\n{ \"theme\": {...}, \"themes\": [{...}] }\n// after\n{ \"themes\": [{...}] }","handlingStrategy":"validation","validationCode":"JsonObject manifest = Json.parse(text).asObject();\nboolean hasTheme = manifest.get(\"theme\") != null;\nboolean hasThemes = manifest.get(\"themes\") != null;\nif (hasTheme == hasThemes) {\n    throw new IllegalArgumentException(\"declare exactly one of 'theme' or 'themes'\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pick one declaration style per manifest: 'theme' for single, 'themes' for multiple.","When converting single-theme packs to multi-theme, remember to remove the old 'theme' key."],"tags":["theme-pack","manifest","json","schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}