[{"data":1,"prerenderedAt":486},["ShallowReactive",2],{"docs-\u002Fmodules\u002Fconfig":3},{"id":4,"title":5,"body":6,"description":476,"extension":477,"meta":478,"navTitle":479,"navigation":480,"path":481,"rawbody":482,"seo":483,"stem":484,"__hash__":485},"docs\u002Fmodules\u002Fconfig.md","Config",{"type":7,"value":8,"toc":452},"minimark",[9,13,17,22,25,28,35,46,52,58,62,68,74,85,91,95,115,126,150,153,161,165,168,174,183,189,193,196,202,206,213,219,222,226,237,243,247,258,261,267,290,297,303,309,316,322,328,331,337,344,350,354,360,363,369,376,382,385,391,394,400,403,409,416,422,426,432,438,442],[10,11,5],"h1",{"id":12},"config",[14,15,16],"p",{},"MagicUtils config maps files to POJOs using annotations while preserving user\ncomments and unknown keys when saving.",[18,19,21],"h2",{"id":20},"why-a-config-manager","Why a config manager",[14,23,24],{},"Loading a config by hand usually means picking a parser, reading keys one by\none, coercing types, filling in defaults for missing keys, and re-serialising\nwithout clobbering the comments the user wrote. Do that across JSON, YAML, and\nTOML and you have three code paths.",[14,26,27],{},"MagicUtils turns a config into a plain Java class: annotate the fields, register\nthe class, and you get typed access with defaults, comments, validation, and\nmigrations. The same model loads from JSON, JSONC, YAML, or TOML, and saving\npreserves user comments and any keys you did not map.",[14,29,30,34],{},[31,32,33],"strong",{},"Before"," (manual, one format, defaults inline everywhere):",[36,37,43],"pre",{"className":38,"code":40,"language":41,"meta":42},[39],"language-java","FileConfiguration yaml = YamlConfiguration.loadConfiguration(file);\nboolean enabled = yaml.getBoolean(\"enabled\", true);\nString greeting = yaml.getString(\"messages.greeting\", \"Hello\");\nint maxPlayers = Math.min(100, Math.max(1, yaml.getInt(\"max-players\", 20)));\n","java","",[44,45,40],"code",{"__ignoreMap":42},[14,47,48,51],{},[31,49,50],{},"After"," (typed model, defaults and bounds on the field, any format):",[36,53,56],{"className":54,"code":55,"language":41,"meta":42},[39],"@ConfigFile(\"example.{ext}\")\npublic final class ExampleConfig {\n    @ConfigValue(\"enabled\")\n    private boolean enabled = true;\n\n    @ConfigValue(\"max-players\")\n    @MinValue(1) @MaxValue(100)\n    private int maxPlayers = 20;\n}\n\nExampleConfig cfg = manager.register(ExampleConfig.class);\n",[44,57,55],{"__ignoreMap":42},[18,59,61],{"id":60},"define-a-config","Define A Config",[36,63,66],{"className":64,"code":65,"language":41,"meta":42},[39],"@ConfigFile(\"example.{ext}\")\n@Comment(\"Example configuration\")\n@ConfigReloadable(sections = {\"messages\"})\npublic final class ExampleConfig {\n    @ConfigValue(\"enabled\")\n    private boolean enabled = true;\n\n    @ConfigSection(\"messages\")\n    private Messages messages = new Messages();\n\n    public static final class Messages {\n        @ConfigValue(\"greeting\")\n        @Comment(\"Greeting shown to players\")\n        private String greeting = \"Hello\";\n    }\n}\n",[44,67,65],{"__ignoreMap":42},[36,69,72],{"className":70,"code":71,"language":41,"meta":42},[39],"ConfigManager manager = new ConfigManager(platform);\nExampleConfig cfg = manager.register(ExampleConfig.class);\n",[44,73,71],{"__ignoreMap":42},[14,75,76,77,80,81,84],{},"If your config path contains placeholders such as ",[44,78,79],{},"{lang}"," or ",[44,82,83],{},"{service}",", pass\nthem during registration:",[36,86,89],{"className":87,"code":88,"language":41,"meta":42},[39],"ExampleConfig cfg = manager.register(ExampleConfig.class, Map.of(\"service\", \"gateway\"));\n",[44,90,88],{"__ignoreMap":42},[18,92,94],{"id":93},"formats-and-selection","Formats And Selection",[96,97,98,102,109],"ul",{},[99,100,101],"li",{},"JSON \u002F JSONC are available out of the box.",[99,103,104,105,108],{},"YAML requires ",[44,106,107],{},"magicutils-config-yaml",".",[99,110,111,112,108],{},"TOML requires ",[44,113,114],{},"magicutils-config-toml",[14,116,117,118,121,122,125],{},"If you use ",[44,119,120],{},"{ext}"," in ",[44,123,124],{},"@ConfigFile",", MagicUtils chooses the extension from the\ncurrent config format rules:",[96,127,128,134,140,145],{},[99,129,130,133],{},[44,131,132],{},"\u003Cconfig>.format"," next to the config",[99,135,136,139],{},[44,137,138],{},"magicutils.format"," in the root config directory",[99,141,142],{},[44,143,144],{},"-Dmagicutils.config.format=jsonc",[99,146,147],{},[44,148,149],{},"MAGICUTILS_CONFIG_FORMAT",[14,151,152],{},"Fabric defaults to JSONC when no explicit format is selected.",[14,154,155,156,108],{},"For advanced format selection and migrations, see\n",[157,158,160],"a",{"href":159},"config-advanced","Config Advanced",[18,162,164],{"id":163},"reloading-and-change-listeners","Reloading And Change Listeners",[14,166,167],{},"Register a listener for live updates:",[36,169,172],{"className":170,"code":171,"language":41,"meta":42},[39],"ListenerSubscription subscription = manager.subscribeChanges(ExampleConfig.class, (config, sections) -> {\n    \u002F\u002F apply live updates\n});\n",[44,173,171],{"__ignoreMap":42},[14,175,176,179,180,108],{},[44,177,178],{},"onChange(...)"," is a lighter variant for when you do not need to unsubscribe: it\nregisters the same kind of listener but returns nothing instead of a\n",[44,181,182],{},"ListenerSubscription",[14,184,185,188],{},[44,186,187],{},"@ConfigReloadable"," restricts which sections can reload at runtime.",[18,190,192],{"id":191},"threading-helpers","Threading Helpers",[14,194,195],{},"Reloading touches disk. Use async or smart helpers on blocking-sensitive\nthreads:",[36,197,200],{"className":198,"code":199,"language":41,"meta":42},[39],"manager.reloadAsync(cfg);\nmanager.reloadAsync(ExampleConfig.class, \"messages\");\nmanager.reloadAllAsync();\n\nmanager.reloadSmart(cfg);\nmanager.reloadSmart(ExampleConfig.class, \"messages\");\nmanager.reloadAllSmart();\n",[44,201,199],{"__ignoreMap":42},[18,203,205],{"id":204},"runtime-managed-config-services","Runtime-Managed Config Services",[14,207,208,209,212],{},"When you already have ",[44,210,211],{},"MagicRuntime",", you can bind a config-backed resource and\nlet MagicUtils rebuild it automatically on matching config reloads:",[36,214,217],{"className":215,"code":216,"language":41,"meta":42},[39],"MagicRuntimeConfigBinding\u003CExampleConfig, ReloadableService> binding = runtime.bindConfig(\n        \"service.example\",\n        ExampleConfig.class,\n        config -> new ReloadableService(config), \u002F\u002F ReloadableService implements AutoCloseable\n        \"messages\"\n);\n\nReloadableService service = binding.require();\n",[44,218,216],{"__ignoreMap":42},[14,220,221],{},"The bound service is also exposed as a named runtime component.",[18,223,225],{"id":224},"migrations","Migrations",[14,227,228,229,232,233,236],{},"Config migrations are declared with ",[44,230,231],{},"ConfigMigration"," and tracked by the\n",[44,234,235],{},"config-version"," key inside the file:",[36,238,241],{"className":239,"code":240,"language":41,"meta":42},[39],"manager.registerMigrations(ExampleConfig.class,\n        new ConfigMigration() {\n            public String fromVersion() { return \"0\"; }\n            public String toVersion() { return \"1\"; }\n            public void migrate(Map\u003CString, Object> root) {\n                root.put(\"enabled\", true);\n            }\n        }\n);\n",[44,242,240],{"__ignoreMap":42},[18,244,246],{"id":245},"validation-annotations","Validation Annotations",[248,249,251,254,255],"h3",{"id":250},"minvalue-maxvalue",[44,252,253],{},"@MinValue"," \u002F ",[44,256,257],{},"@MaxValue",[14,259,260],{},"Clamp numeric fields to a safe range. Values outside the range are automatically\nadjusted when the config is loaded. A warning is logged by default.",[36,262,265],{"className":263,"code":264,"language":41,"meta":42},[39],"@ConfigValue(\"retry_interval\")\n@MinValue(5)\n@Comment(\"Retry interval in seconds (minimum: 5)\")\nprivate int retryInterval = 10;\n\n@ConfigValue(\"max_players\")\n@MaxValue(100)\n@Comment(\"Maximum players (maximum: 100)\")\nprivate int maxPlayers = 20;\n",[44,266,264],{"__ignoreMap":42},[14,268,269,270,273,274,273,277,273,280,273,283,273,286,289],{},"Supported types: ",[44,271,272],{},"byte",", ",[44,275,276],{},"short",[44,278,279],{},"int",[44,281,282],{},"long",[44,284,285],{},"float",[44,287,288],{},"double"," and their\nwrapper types.",[14,291,292,293,296],{},"Set ",[44,294,295],{},"warn = false"," to suppress the clamping log message:",[36,298,301],{"className":299,"code":300,"language":41,"meta":42},[39],"@MinValue(value = 0, warn = false)\n",[44,302,300],{"__ignoreMap":42},[248,304,306],{"id":305},"configvaluerequired-true",[44,307,308],{},"@ConfigValue(required = true)",[14,310,311,312,315],{},"Marks a key as mandatory. If it is missing from the file when the config loads,\nMagicUtils throws an ",[44,313,314],{},"IllegalStateException"," instead of falling back to the field\ndefault:",[36,317,320],{"className":318,"code":319,"language":41,"meta":42},[39],"@ConfigValue(value = \"api_token\", required = true)\nprivate String apiToken;\n",[44,321,319],{"__ignoreMap":42},[248,323,325],{"id":324},"defaultvalue",[44,326,327],{},"@DefaultValue",[14,329,330],{},"Provides a default string value for a config field when the key is missing from\nthe file:",[36,332,335],{"className":333,"code":334,"language":41,"meta":42},[39],"@ConfigValue(\"channel\")\n@DefaultValue(\"stable\")\nprivate String channel;\n",[44,336,334],{"__ignoreMap":42},[14,338,339,340,343],{},"For dynamic defaults, implement ",[44,341,342],{},"DefaultValueProvider\u003CT>"," and reference it:",[36,345,348],{"className":346,"code":347,"language":41,"meta":42},[39],"@ConfigValue(\"name\")\n@DefaultValue(provider = MyDefaultProvider.class)\nprivate String name;\n",[44,349,347],{"__ignoreMap":42},[18,351,353],{"id":352},"serializable-types","Serializable Types",[248,355,357],{"id":356},"configserializable",[44,358,359],{},"@ConfigSerializable",[14,361,362],{},"Marks a class so it can be used in config lists and maps:",[36,364,367],{"className":365,"code":366,"language":41,"meta":42},[39],"@ConfigSerializable\npublic class ServerEntry {\n    @ConfigValue(\"name\")\n    private String name = \"\";\n\n    @ConfigValue(\"port\")\n    private int port = 25565;\n}\n",[44,368,366],{"__ignoreMap":42},[14,370,371,372,375],{},"Use ",[44,373,374],{},"includeNulls = true"," to serialize null fields explicitly.",[248,377,379],{"id":378},"saveto",[44,380,381],{},"@SaveTo",[14,383,384],{},"Redirects a field to a different file:",[36,386,389],{"className":387,"code":388,"language":41,"meta":42},[39],"@ConfigValue(\"secrets\")\n@SaveTo(\"secrets.{ext}\")\nprivate Secrets secrets = new Secrets();\n",[44,390,388],{"__ignoreMap":42},[14,392,393],{},"The path is relative to the plugin data folder.",[248,395,397],{"id":396},"listprocessor",[44,398,399],{},"@ListProcessor",[14,401,402],{},"Applies per-item validation or transformation when loading list fields:",[36,404,407],{"className":405,"code":406,"language":41,"meta":42},[39],"@ConfigValue(\"servers\")\n@ListProcessor(ServerListProcessor.class)\nprivate List\u003CServerEntry> servers = new ArrayList\u003C>();\n",[44,408,406],{"__ignoreMap":42},[14,410,411,412,415],{},"The processor implements ",[44,413,414],{},"ListItemProcessor\u003CT>",":",[36,417,420],{"className":418,"code":419,"language":41,"meta":42},[39],"public class ServerListProcessor implements ListItemProcessor\u003CServerEntry> {\n    @Override\n    public ProcessResult\u003CServerEntry> process(ServerEntry item, int index) {\n        if (item.name == null || item.name.isBlank()) {\n            return ProcessResult.replaceWithDefault();\n        }\n        return ProcessResult.ok(item);\n    }\n}\n",[44,421,419],{"__ignoreMap":42},[18,423,425],{"id":424},"custom-value-adapters","Custom Value Adapters",[14,427,428,429,415],{},"Register serializers via ",[44,430,431],{},"ConfigAdapters.register(...)",[36,433,436],{"className":434,"code":435,"language":41,"meta":42},[39],"ConfigAdapters.register(Duration.class, new ConfigValueAdapter\u003C>() {\n    public Duration deserialize(Object value) { ... }\n    public Object serialize(Duration value) { ... }\n});\n",[44,437,435],{"__ignoreMap":42},[18,439,441],{"id":440},"shutdown","Shutdown",[14,443,444,445,447,448,451],{},"Bootstrap helpers and ",[44,446,211],{}," can manage the config manager lifecycle\nfor you. In manual setups, call ",[44,449,450],{},"ConfigManager.shutdown()"," during plugin or mod\nshutdown to stop file watchers cleanly.",{"title":42,"searchDepth":453,"depth":453,"links":454},3,[455,457,458,459,460,461,462,463,469,474,475],{"id":20,"depth":456,"text":21},2,{"id":60,"depth":456,"text":61},{"id":93,"depth":456,"text":94},{"id":163,"depth":456,"text":164},{"id":191,"depth":456,"text":192},{"id":204,"depth":456,"text":205},{"id":224,"depth":456,"text":225},{"id":245,"depth":456,"text":246,"children":464},[465,467,468],{"id":250,"depth":453,"text":466},"@MinValue \u002F @MaxValue",{"id":305,"depth":453,"text":308},{"id":324,"depth":453,"text":327},{"id":352,"depth":456,"text":353,"children":470},[471,472,473],{"id":356,"depth":453,"text":359},{"id":378,"depth":453,"text":381},{"id":396,"depth":453,"text":399},{"id":424,"depth":456,"text":425},{"id":440,"depth":456,"text":441},"The MagicUtils config manager: JSON\u002FJSONC out of the box, optional YAML and TOML, plus migrations, defaults, and typed access.","md",{},null,true,"\u002Fmodules\u002Fconfig","---\ntitle: Config\ndescription: 'The MagicUtils config manager: JSON\u002FJSONC out of the box, optional YAML and TOML, plus migrations, defaults, and typed access.'\n---\n\n# Config\n\nMagicUtils config maps files to POJOs using annotations while preserving user\ncomments and unknown keys when saving.\n\n## Why a config manager\n\nLoading a config by hand usually means picking a parser, reading keys one by\none, coercing types, filling in defaults for missing keys, and re-serialising\nwithout clobbering the comments the user wrote. Do that across JSON, YAML, and\nTOML and you have three code paths.\n\nMagicUtils turns a config into a plain Java class: annotate the fields, register\nthe class, and you get typed access with defaults, comments, validation, and\nmigrations. The same model loads from JSON, JSONC, YAML, or TOML, and saving\npreserves user comments and any keys you did not map.\n\n**Before** (manual, one format, defaults inline everywhere):\n\n```java\nFileConfiguration yaml = YamlConfiguration.loadConfiguration(file);\nboolean enabled = yaml.getBoolean(\"enabled\", true);\nString greeting = yaml.getString(\"messages.greeting\", \"Hello\");\nint maxPlayers = Math.min(100, Math.max(1, yaml.getInt(\"max-players\", 20)));\n```\n\n**After** (typed model, defaults and bounds on the field, any format):\n\n```java\n@ConfigFile(\"example.{ext}\")\npublic final class ExampleConfig {\n    @ConfigValue(\"enabled\")\n    private boolean enabled = true;\n\n    @ConfigValue(\"max-players\")\n    @MinValue(1) @MaxValue(100)\n    private int maxPlayers = 20;\n}\n\nExampleConfig cfg = manager.register(ExampleConfig.class);\n```\n\n## Define A Config\n\n```java\n@ConfigFile(\"example.{ext}\")\n@Comment(\"Example configuration\")\n@ConfigReloadable(sections = {\"messages\"})\npublic final class ExampleConfig {\n    @ConfigValue(\"enabled\")\n    private boolean enabled = true;\n\n    @ConfigSection(\"messages\")\n    private Messages messages = new Messages();\n\n    public static final class Messages {\n        @ConfigValue(\"greeting\")\n        @Comment(\"Greeting shown to players\")\n        private String greeting = \"Hello\";\n    }\n}\n```\n\n```java\nConfigManager manager = new ConfigManager(platform);\nExampleConfig cfg = manager.register(ExampleConfig.class);\n```\n\nIf your config path contains placeholders such as `{lang}` or `{service}`, pass\nthem during registration:\n\n```java\nExampleConfig cfg = manager.register(ExampleConfig.class, Map.of(\"service\", \"gateway\"));\n```\n\n## Formats And Selection\n\n- JSON \u002F JSONC are available out of the box.\n- YAML requires `magicutils-config-yaml`.\n- TOML requires `magicutils-config-toml`.\n\nIf you use `{ext}` in `@ConfigFile`, MagicUtils chooses the extension from the\ncurrent config format rules:\n\n- `\u003Cconfig>.format` next to the config\n- `magicutils.format` in the root config directory\n- `-Dmagicutils.config.format=jsonc`\n- `MAGICUTILS_CONFIG_FORMAT`\n\nFabric defaults to JSONC when no explicit format is selected.\n\nFor advanced format selection and migrations, see\n[Config Advanced](config-advanced.md).\n\n## Reloading And Change Listeners\n\nRegister a listener for live updates:\n\n```java\nListenerSubscription subscription = manager.subscribeChanges(ExampleConfig.class, (config, sections) -> {\n    \u002F\u002F apply live updates\n});\n```\n\n`onChange(...)` is a lighter variant for when you do not need to unsubscribe: it\nregisters the same kind of listener but returns nothing instead of a\n`ListenerSubscription`.\n\n`@ConfigReloadable` restricts which sections can reload at runtime.\n\n## Threading Helpers\n\nReloading touches disk. Use async or smart helpers on blocking-sensitive\nthreads:\n\n```java\nmanager.reloadAsync(cfg);\nmanager.reloadAsync(ExampleConfig.class, \"messages\");\nmanager.reloadAllAsync();\n\nmanager.reloadSmart(cfg);\nmanager.reloadSmart(ExampleConfig.class, \"messages\");\nmanager.reloadAllSmart();\n```\n\n## Runtime-Managed Config Services\n\nWhen you already have `MagicRuntime`, you can bind a config-backed resource and\nlet MagicUtils rebuild it automatically on matching config reloads:\n\n```java\nMagicRuntimeConfigBinding\u003CExampleConfig, ReloadableService> binding = runtime.bindConfig(\n        \"service.example\",\n        ExampleConfig.class,\n        config -> new ReloadableService(config), \u002F\u002F ReloadableService implements AutoCloseable\n        \"messages\"\n);\n\nReloadableService service = binding.require();\n```\n\nThe bound service is also exposed as a named runtime component.\n\n## Migrations\n\nConfig migrations are declared with `ConfigMigration` and tracked by the\n`config-version` key inside the file:\n\n```java\nmanager.registerMigrations(ExampleConfig.class,\n        new ConfigMigration() {\n            public String fromVersion() { return \"0\"; }\n            public String toVersion() { return \"1\"; }\n            public void migrate(Map\u003CString, Object> root) {\n                root.put(\"enabled\", true);\n            }\n        }\n);\n```\n\n## Validation Annotations\n\n### `@MinValue` \u002F `@MaxValue`\n\nClamp numeric fields to a safe range. Values outside the range are automatically\nadjusted when the config is loaded. A warning is logged by default.\n\n```java\n@ConfigValue(\"retry_interval\")\n@MinValue(5)\n@Comment(\"Retry interval in seconds (minimum: 5)\")\nprivate int retryInterval = 10;\n\n@ConfigValue(\"max_players\")\n@MaxValue(100)\n@Comment(\"Maximum players (maximum: 100)\")\nprivate int maxPlayers = 20;\n```\n\nSupported types: `byte`, `short`, `int`, `long`, `float`, `double` and their\nwrapper types.\n\nSet `warn = false` to suppress the clamping log message:\n\n```java\n@MinValue(value = 0, warn = false)\n```\n\n### `@ConfigValue(required = true)`\n\nMarks a key as mandatory. If it is missing from the file when the config loads,\nMagicUtils throws an `IllegalStateException` instead of falling back to the field\ndefault:\n\n```java\n@ConfigValue(value = \"api_token\", required = true)\nprivate String apiToken;\n```\n\n### `@DefaultValue`\n\nProvides a default string value for a config field when the key is missing from\nthe file:\n\n```java\n@ConfigValue(\"channel\")\n@DefaultValue(\"stable\")\nprivate String channel;\n```\n\nFor dynamic defaults, implement `DefaultValueProvider\u003CT>` and reference it:\n\n```java\n@ConfigValue(\"name\")\n@DefaultValue(provider = MyDefaultProvider.class)\nprivate String name;\n```\n\n## Serializable Types\n\n### `@ConfigSerializable`\n\nMarks a class so it can be used in config lists and maps:\n\n```java\n@ConfigSerializable\npublic class ServerEntry {\n    @ConfigValue(\"name\")\n    private String name = \"\";\n\n    @ConfigValue(\"port\")\n    private int port = 25565;\n}\n```\n\nUse `includeNulls = true` to serialize null fields explicitly.\n\n### `@SaveTo`\n\nRedirects a field to a different file:\n\n```java\n@ConfigValue(\"secrets\")\n@SaveTo(\"secrets.{ext}\")\nprivate Secrets secrets = new Secrets();\n```\n\nThe path is relative to the plugin data folder.\n\n### `@ListProcessor`\n\nApplies per-item validation or transformation when loading list fields:\n\n```java\n@ConfigValue(\"servers\")\n@ListProcessor(ServerListProcessor.class)\nprivate List\u003CServerEntry> servers = new ArrayList\u003C>();\n```\n\nThe processor implements `ListItemProcessor\u003CT>`:\n\n```java\npublic class ServerListProcessor implements ListItemProcessor\u003CServerEntry> {\n    @Override\n    public ProcessResult\u003CServerEntry> process(ServerEntry item, int index) {\n        if (item.name == null || item.name.isBlank()) {\n            return ProcessResult.replaceWithDefault();\n        }\n        return ProcessResult.ok(item);\n    }\n}\n```\n\n## Custom Value Adapters\n\nRegister serializers via `ConfigAdapters.register(...)`:\n\n```java\nConfigAdapters.register(Duration.class, new ConfigValueAdapter\u003C>() {\n    public Duration deserialize(Object value) { ... }\n    public Object serialize(Duration value) { ... }\n});\n```\n\n## Shutdown\n\nBootstrap helpers and `MagicRuntime` can manage the config manager lifecycle\nfor you. In manual setups, call `ConfigManager.shutdown()` during plugin or mod\nshutdown to stop file watchers cleanly.\n",{"title":5,"description":476},"modules\u002Fconfig","6OMvPd3vwcJYpopjooJykgXbLfTYYMk7FLJEX96Jr2U",1783944487695]