{"record":{"id":"2922707cc2605079","repo":"HMCL-dev/HMCL","slug":"portablepath-must-be-a-string-in-peek","errorCode":null,"errorMessage":"PortablePath must be a string: \" + in.peek()","messagePattern":"PortablePath must be a string: \" \\+ in\\.peek\\(\\)","errorType":"validation","errorClass":"JsonParseException","httpStatus":null,"severity":"error","filePath":"HMCLCore/src/main/java/org/jackhuang/hmcl/util/PortablePath.java","lineNumber":138,"sourceCode":"\n    /// Gson adapter that serializes portable paths as strings.\n    @NotNullByDefault\n    public static final class Adapter extends TypeAdapter<@Nullable PortablePath> {\n        /// Writes a portable path as its stored path string, or JSON null when the value is null.\n        @Override\n        public void write(JsonWriter out, @Nullable PortablePath value) throws IOException {\n            out.value(value == null ? null : value.getPath());\n        }\n\n        /// Reads a portable path from a string or JSON null.\n        @Override\n        public @Nullable PortablePath read(JsonReader in) throws IOException {\n            if (in.peek() == JsonToken.NULL) {\n                in.nextNull();\n                return null;\n            }\n            if (in.peek() != JsonToken.STRING) {\n                throw new JsonParseException(\"PortablePath must be a string: \" + in.peek());\n            }\n\n            return PortablePath.of(in.nextString());\n        }\n    }\n}\n","sourceCodeStart":120,"sourceCodeEnd":145,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCLCore/src/main/java/org/jackhuang/hmcl/util/PortablePath.java#L120-L145","documentation":"PortablePath's Gson TypeAdapter expects the JSON value for a path field to be a JSON string. This JsonParseException is thrown when the reader encounters a non-string, non-null token (number, boolean, object, array), since a PortablePath can only be deserialized from a string.","triggerScenarios":"Deserializing JSON (e.g. modpack/instance config) where a field mapped to PortablePath holds a number, boolean, object, or array instead of a string — usually a schema drift or corrupted/hand-edited config.","commonSituations":"Hand-edited config files quoting inconsistently; a schema change where a path field became an object; automated tools writing typed values into path fields.","solutions":["Fix the JSON so the path field is a string (add quotes around the value)","Locate which file/field failed — the message includes the offending JsonToken — and correct that entry","Restore the config from a known-good copy","If a schema legitimately changed, update the adapter or mapping to handle the new shape"],"exampleFix":"// before\n{ \"path\": 123 }\n// after\n{ \"path\": \".minecraft/mods\" }","handlingStrategy":"validation","validationCode":"// before Gson parsing, sanity-check path fields are strings\nfor (Map.Entry<String, JsonElement> e : obj.entrySet())\n    if (e.getKey().toLowerCase().endsWith(\"path\") && !e.getValue().isJsonPrimitive())\n        throw new JsonSyntaxException(e.getKey() + \" must be a string\");","typeGuard":"static boolean isPortablePathJson(JsonElement el) {\n    return el != null && el.isJsonPrimitive() && el.getAsJsonPrimitive().isString();\n}","tryCatchPattern":"try {\n    PortablePath p = gson.fromJson(json, Config.class).path;\n} catch (JsonParseException e) {\n    if (e.getMessage().startsWith(\"PortablePath must be a string\")) restoreDefaultConfig();\n    else throw e;\n}","preventionTips":["Validate config JSON against a schema before parsing","Quote all path values in hand-edited configs","Version config schemas and migrate rather than silently changing field types"],"tags":["json","gson","deserialization","path"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}