Missing json sub fields (or adding new config sub fields) crashes config screen due to deserializing with gson without default constructors
kevinthegreat1 opened this issue · 6 comments
What I wanted
I have an existing config class:
public static class QuickNavItem {
@SerialEntry
public boolean render = true;
@SerialEntry
public String uiTitle;
@SerialEntry
public String clickEvent;
}
which serializes as such:
"render": true,
"uiTitle": "Your Skills",
"clickEvent": "/skills"
I want to add a new field so my config looks like this:
public static class QuickNavItem {
@SerialEntry
public boolean render = true;
@SerialEntry
public String uiTitle;
@SerialEntry
public String tooltip;
@SerialEntry
public String clickEvent;
}
What I tried
I've tried setting a default value to the tooltip
field, adding required = false
, or adding nullable = true
.
What I encountered
All of the options above fails to load the old config file with the following error:
[23:46:20] [Render thread/ERROR] (Minecraft) Reported exception thrown!
net.minecraft.util.crash.CrashException: mouseClicked event handler
at net.minecraft.client.gui.screen.Screen.wrapScreenError(Screen.java:467) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.Mouse.onMouseButton(Mouse.java:96) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.Mouse.method_22686(Mouse.java:192) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.util.thread.ThreadExecutor.execute(ThreadExecutor.java:90) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.Mouse.method_22684(Mouse.java:192) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at org.lwjgl.glfw.GLFWMouseButtonCallbackI.callback(GLFWMouseButtonCallbackI.java:43) ~[lwjgl-glfw-3.3.3.jar:?]
at org.lwjgl.system.JNI.invokeV(Native Method) ~[lwjgl-3.3.3.jar:?]
at org.lwjgl.glfw.GLFW.glfwWaitEventsTimeout(GLFW.java:3509) ~[lwjgl-glfw-3.3.3.jar:?]
at com.mojang.blaze3d.systems.RenderSystem.limitDisplayFPS(RenderSystem.java:160) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.MinecraftClient.render(MinecraftClient.java:1295) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.MinecraftClient.run(MinecraftClient.java:884) [minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.main.Main.main(Main.java:228) [minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider.launch(MinecraftGameProvider.java:470) [fabric-loader-0.15.11.jar:?]
at net.fabricmc.loader.impl.launch.knot.Knot.launch(Knot.java:74) [fabric-loader-0.15.11.jar:?]
at net.fabricmc.loader.impl.launch.knot.KnotClient.main(KnotClient.java:23) [fabric-loader-0.15.11.jar:?]
at net.fabricmc.devlaunchinjector.Main.main(Main.java:86) [dev-launch-injector-0.2.1+build.8.jar:?]
Caused by: java.lang.NullPointerException: Cannot invoke "String.isEmpty()" because "this.inputField" is null
at dev.isxander.yacl3.gui.controllers.string.StringControllerElement.getValueText(StringControllerElement.java:461) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.controllers.string.StringControllerElement.setDimension(StringControllerElement.java:436) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.controllers.string.StringControllerElement.<init>(StringControllerElement.java:45) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.controllers.string.IStringController.provideWidget(IStringController.java:42) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.OptionListWidget.refreshOptions(OptionListWidget.java:75) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.OptionListWidget.<init>(OptionListWidget.java:40) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.YACLScreen$CategoryTab.<init>(YACLScreen.java:359) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at dev.isxander.yacl3.gui.YACLScreen.lambda$init$4(YACLScreen.java:89) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024) ~[?:?]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[?:?]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575) ~[?:?]
at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260) ~[?:?]
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616) ~[?:?]
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622) ~[?:?]
at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627) ~[?:?]
at dev.isxander.yacl3.gui.YACLScreen.init(YACLScreen.java:90) ~[yet-another-config-lib-3.5.0+1.21-fabric.jar:?]
at net.minecraft.client.gui.screen.Screen.init(Screen.java:324) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.MinecraftClient.setScreen(MinecraftClient.java:1134) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at com.terraformersmc.modmenu.gui.widget.entries.ModListEntry.openConfig(ModListEntry.java:120) ~[modmenu-11.0.0-beta.1.jar:?]
at com.terraformersmc.modmenu.gui.widget.entries.ModListEntry.mouseClicked(ModListEntry.java:110) ~[modmenu-11.0.0-beta.1.jar:?]
at com.terraformersmc.modmenu.gui.widget.ModListWidget.mouseClicked(ModListWidget.java:270) ~[modmenu-11.0.0-beta.1.jar:?]
at net.minecraft.client.gui.ParentElement.mouseClicked(ParentElement.java:45) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
at net.minecraft.client.Mouse.method_1611(Mouse.java:96) ~[minecraft-merged-f6269c2542-1.21-loom.mappings.1_21.layered+hash.998484755-v2.jar:?]
... 16 more
I figured it out. First, this is a sub class, so this is serialized and deserialized with gson. Turns out having a constructor does matter. It means that there is no default constructor, and gson skips initialization if there are no default constructors available, which means the default values do not apply.
Solution
Have default values for the fields.
And either:
- Have no constructor.
- Have a default constructor available (one with no args).
Future steps
I don't think there's much yacl van do about this, except warn users about this behavior.
Originally reported SkyblockerMod/Skyblocker#890 and here SkyblockerMod/Skyblocker#894.
In general, we've encountered many problems when the config simply refuses to load and crashes when it tries to deserialize a null or nonexistent field. I believe there should be alternative options such as default values instead of crashing.
Is a default option not specified by just giving the field an initial value?
No. I also do have a constructor for this class, and I’ve tried both adding a default value to the field and making sure the constructor doesn’t set a null value. But to my knowledge, the constructor shouldn’t get called while deserializing from json?