HMCL-dev/HMCL · error · JsonParseException
Config is not an object:
Error message
Config is not an object:
What it means
The custom Gson deserializer for ObservableSetting subclasses requires the incoming JSON to be a JSON object, since each key is mapped to an ObservableField of the setting instance. When the JSON element is present but is an array, string, number, boolean, or other non-object value, the deserializer aborts with this JsonParseException before creating the instance.
Solutions
- Inspect the JSON input and ensure the value at that position is a JSON object with keys matching the ObservableSetting fields.
- Validate the JSON shape (e.g. element.isJsonObject()) before handing it to Gson.
- Restore the config file from backup or regenerate it if it was corrupted or hand-mangled.
Example fix
// before
gson.fromJson("[1,2,3]", MySetting.class); // throws 'Config is not an object'
// after
JsonElement el = JsonParser.parseString(raw);
if (el.isJsonObject()) {
MySetting s = gson.fromJson(el, MySetting.class);
} Defensive patterns
Strategy: type-guard
Validate before calling
JsonElement el = JsonParser.parseString(raw);
if (!el.isJsonObject()) throw new IllegalArgumentException("Expected JSON object for config"); Type guard
static boolean isSettingJson(JsonElement el) { return el != null && el.isJsonObject(); } Try / catch
try { return gson.fromJson(raw, MySetting.class); } catch (JsonParseException e) { log.error("Bad config shape", e); return defaultSetting(); } Prevention
- Validate JSON shape before deserialization.
- Keep config files generated by the app, not hand-edited.
- Keep a backup of known-good config files.
When it happens
Trigger: Gson deserializes a JSON document whose top-level (or nested, for a field of an ObservableSetting type) value is not an object — e.g. a JSON array, bare string, or number is fed to the adapter's deserialize() method.
Common situations: Hand-edited or corrupted config files where the settings object was replaced by an array or scalar; an API or tool emitting the wrong shape; deserializing the wrong type into an ObservableSetting-typed field.
Related errors
- json.toString()
- Protected payload is not a
- Expected JsonObject but got
- PortablePath must be a string: " + in.peek()
- this.getClass() + " cannot be deserialized to " + type
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/3e7ec70d82349afb.
Report an issue: GitHub.
Appendix: source
Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/ObservableSetting.java:369
JsonObject result = new JsonObject();
for (var field : fields) {
Observable observable = field.get(setting);
if (setting.tracker.isDirty(observable)) {
field.serialize(result, setting, context);
}
}
setting.unknownFields.forEach(result::add);
return result;
}
@Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json.isJsonNull())
return null;
if (!json.isJsonObject())
throw new JsonParseException("Config is not an object: " + json);
T setting = createInstance();
@SuppressWarnings("unchecked")
var fields = (List<ObservableField<T>>) FIELDS.get(setting.getClass());
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
for (ObservableField<T> field : fields) {
JsonElement value = values.remove(field.getSerializedName());
if (value == null) {
for (String alternateName : field.getAlternateNames()) {
value = values.remove(alternateName);
if (value != null)
break;
}
}
if (value != null) {
setting.tracker.markDirty(field.get(setting));View on GitHub (pinned to 24702dc5a0)