Skip to content

Repository files navigation

ezconfig

A lightweight, easy-to-use JSON configuration library for Java.

Define a config class, annotate it, and load it.

@Config("name.json")
@DatapublicfinalclassNameConfig {
privateStringname = "ezconfig";
privateintmaxPlayers = 100;
}
NameConfigconfig = ConfigLoader.loadSafe(NameConfig.class);

That's it.


Features

  • Simple annotation-based configuration
  • JSON powered by Gson
  • Automatic config creation
  • Default value merging
  • Nested config objects
  • Lists, sets, maps and enums
  • Automatic .json extension
  • Atomic file writes
  • Broken config backups
  • Config caching
  • Reload support
  • Custom base directories
  • Custom JSON field names
  • Ignored runtime fields
  • Unknown JSON fields are preserved
  • No dependency injection or framework required

Requirements

  • Java 25+

Installation

Gradle Kotlin DSL

dependencies {
implementation("dev.mzcy:config:1.0.0")
}

If you are using the project directly:

dependencies {
implementation(project(":config"))
}

Getting Started

Create a config class:

importdev.mzcy.config.annotation.Config;
importlombok.Data;
@Data@Config("server.json")
publicfinalclassServerConfig {
privateStringname = "ezconfig";
privateintmaxPlayers = 100;
privatebooleanmaintenance = false;
}

Load it:

ServerConfigconfig =
ConfigLoader.loadSafe(ServerConfig.class);

The following file will automatically be created:

server.json
{
"name": "ezconfig",
"maxPlayers": 100,
"maintenance": false
}

ConfigLoader

ConfigLoader provides the simple static API.

Load

ServerConfigconfig =
ConfigLoader.load(ServerConfig.class);

load() is strict and throws config exceptions when the file cannot be loaded or parsed.


Safe Load

ServerConfigconfig =
ConfigLoader.loadSafe(ServerConfig.class);

loadSafe() is intended for normal application usage.

If the config does not exist, it is created automatically.

If the JSON is broken, the existing file is backed up and a fresh config is generated from the Java defaults.


Get

ServerConfigconfig =
ConfigLoader.get(ServerConfig.class);

Returns the cached config.

If the config has not been loaded yet, it is loaded automatically using loadSafe().


Save

config.setMaintenance(true);
ConfigLoader.save(config);

You can also save the cached instance by class:

ConfigLoader.save(ServerConfig.class);

Reload

ServerConfigconfig =
ConfigLoader.reload(ServerConfig.class);

Or safely:

ServerConfigconfig =
ConfigLoader.reloadSafe(ServerConfig.class);

The existing cached instance is removed and the config is loaded again from disk.

Note

Existing Java references still point to the old object after a reload.


Unload

ConfigLoader.unload(ServerConfig.class);

Clear Cache

ConfigLoader.clearCache();

Base Directory

By default, configs are resolved relative to the current working directory.

You can configure a custom base directory:

ConfigLoader.configure(
Path.of("data")
);

Given:

@Config("server.json")
publicfinalclassServerConfig {
}

the resulting path becomes:

data/server.json

Minecraft Example

For a Bukkit/Paper plugin:

@OverridepublicvoidonEnable() {
ConfigLoader.configure(
getDataFolder().toPath()
);
ServerConfigconfig =
ConfigLoader.loadSafe(
ServerConfig.class
);
}

This creates:

plugins/
└── YourPlugin/
└── server.json

Config Directories

Configs can specify an additional directory:

@Config(
value = "database.json",
directory = "storage"
)
@DatapublicfinalclassDatabaseConfig {
privateStringhost = "127.0.0.1";
privateintport = 27017;
}

With a base directory of:

plugins/YourPlugin

this creates:

plugins/
└── YourPlugin/
└── storage/
└── database.json

Automatic .json Extension

Both forms are valid:

@Config("server.json")

and:

@Config("server")

Both resolve to:

server.json

Default Values

Default values should be defined directly inside the config class.

@Config("server.json")
@DatapublicfinalclassServerConfig {
privateStringname = "Skyliro";
privateintmaxPlayers = 100;
privatebooleanmaintenance = false;
}

The Java class acts as the default configuration.


Automatic Default Merging

One of the main features of Config is automatic default merging.

Suppose an existing config contains:

{
"name": "MyNetwork"
}

and the Java class later becomes:

@Config("server.json")
@DatapublicfinalclassServerConfig {
privateStringname = "Skyliro";
privateintmaxPlayers = 100;
privatebooleanmaintenance = false;
}

On the next load, the config is automatically updated to:

{
"name": "MyNetwork",
"maxPlayers": 100,
"maintenance": false
}

Existing values always win.

Defaults are only inserted when properties are missing.


Nested Objects

No additional section annotation is required.

@Config("network.json")
@DatapublicfinalclassNetworkConfig {
privateGeneralgeneral = newGeneral();
privateDatabasedatabase = newDatabase();
@DatapublicstaticfinalclassGeneral {
privateStringname = "Skyliro";
privateintmaxPlayers = 100;
}
@DatapublicstaticfinalclassDatabase {
privateStringhost = "127.0.0.1";
privateintport = 27017;
}
}

Result:

{
"general": {
"name": "Skyliro",
"maxPlayers": 100
},
"database": {
"host": "127.0.0.1",
"port": 27017
}
}

Nested objects are recursively merged when new default properties are introduced.


Collections

Standard Java collections are supported.

@Data@Config("messages.json")
publicfinalclassMessagesConfig {
privateList<String> messages =
newArrayList<>(
List.of(
"Hello!",
"Welcome!"
)
);
}

Result:

{
"messages": [
"Hello!",
"Welcome!"
]
}

For mutable config values, mutable collections are recommended.

Prefer:

newArrayList<>(List.of("Hello"))

over:

List.of("Hello")

if you intend to modify the collection at runtime.


Enums

Enums are stored using their names.

publicenumPlayerCountMode {
NETWORK,
SERVER,
FIXED
}
@Config("proxy.json")
@DatapublicfinalclassProxyConfig {
privatePlayerCountModeplayerCountMode =
PlayerCountMode.NETWORK;
}

Result:

{
"playerCountMode": "NETWORK"
}

Custom Field Names

Use @ConfigName to change the JSON property name without exposing Gson annotations.

@Config("server.json")
@DatapublicfinalclassServerConfig {
@ConfigName("max-players")
privateintmaxPlayers = 100;
@ConfigName("maintenance-mode")
privatebooleanmaintenance = false;
}

Result:

{
"max-players": 100,
"maintenance-mode": false
}

Ignored Fields

Use @ConfigIgnore for runtime-only values.

@Config("server.json")
@DatapublicfinalclassServerConfig {
privateStringname = "Skyliro";
@ConfigIgnoreprivatelongloadedAt =
System.currentTimeMillis();
}

Result:

{
"name": "Skyliro"
}

loadedAt remains available in Java but is neither serialized nor deserialized.

Java transient fields are also ignored by Gson.


Unknown Properties

Unknown properties are preserved whenever possible.

Given:

{
"name": "Skyliro",
"oldSetting": true
}

and:

@Config("server.json")
@DatapublicfinalclassServerConfig {
privateStringname = "Skyliro";
}

loading and saving the config will not immediately remove oldSetting.

This helps avoid destructive changes during upgrades and downgrades.


Arrays and Lists During Merging

Arrays are treated as complete user-defined values.

Default:

{
"messages": [
"Hello",
"Welcome"
]
}

Existing config:

{
"messages": [
"Custom message"
]
}

Result:

{
"messages": [
"Custom message"
]
}

The library does not append default array entries to customized arrays.


Null Values

Explicit null values are considered existing values.

Default:

{
"name": "Skyliro"
}

Existing config:

{
"name": null
}

The value remains:

{
"name": null
}

Broken Config Recovery

loadSafe() automatically handles invalid JSON.

For example, if:

server.json

contains malformed JSON, it will first be preserved as something similar to:

server.broken-20260816-142012-531.json

A fresh server.json is then created using the Java defaults.

This allows the application to continue starting without silently destroying the broken file.


Atomic Writes

Configs are written through a temporary file first.

Conceptually:

server.json
↓
server.json.tmp
↓
atomic move
↓
server.json

If the filesystem does not support atomic moves, Config automatically falls back to a normal replacement move.

This reduces the chance of leaving partially written configuration files behind.


ConfigManager

Applications that do not want global static state can use ConfigManager directly.

ConfigManagermanager =
newConfigManager(
Path.of("configs")
);
ServerConfigconfig =
manager.loadSafe(
ServerConfig.class
);

The same operations are available:

manager.load(ServerConfig.class);
manager.loadSafe(ServerConfig.class);
manager.get(ServerConfig.class);
manager.save(config);
manager.save(ServerConfig.class);
manager.reload(ServerConfig.class);
manager.reloadSafe(ServerConfig.class);
manager.unload(ServerConfig.class);
manager.clearCache();

ConfigLoader is simply the convenient global facade around a ConfigManager.


Example

importdev.mzcy.config.ConfigLoader;
importdev.mzcy.config.annotation.Config;
importdev.mzcy.config.annotation.ConfigIgnore;
importdev.mzcy.config.annotation.ConfigName;
importlombok.Data;
importjava.util.ArrayList;
importjava.util.List;
@Data@Config("network.json")
publicfinalclassNetworkConfig {
privateStringname = "Skyliro";
@ConfigName("max-players")
privateintmaxPlayers = 100;
privatebooleanmaintenance = false;
privateDatabasedatabase =
newDatabase();
privateList<String> messages =
newArrayList<>(
List.of(
"Welcome!",
"Have fun!"
)
);
@ConfigIgnoreprivatelongloadedAt =
System.currentTimeMillis();
@DatapublicstaticfinalclassDatabase {
privateStringhost = "127.0.0.1";
privateintport = 27017;
privateStringdatabase = "skyliro";
}
}

Load it:

NetworkConfigconfig =
ConfigLoader.loadSafe(
NetworkConfig.class
);

Modify it:

config.setMaxPlayers(250);
config.setMaintenance(true);
ConfigLoader.save(config);

Reload it:

config =
ConfigLoader.reloadSafe(
NetworkConfig.class
);

API Overview

ConfigLoader.load(ConfigClass.class);
ConfigLoader.loadSafe(ConfigClass.class);
ConfigLoader.get(ConfigClass.class);
ConfigLoader.save(config);
ConfigLoader.save(ConfigClass.class);
ConfigLoader.reload(ConfigClass.class);
ConfigLoader.reloadSafe(ConfigClass.class);
ConfigLoader.isLoaded(ConfigClass.class);
ConfigLoader.unload(ConfigClass.class);
ConfigLoader.clearCache();
ConfigLoader.configure(Path.of("configs"));

Project Structure

dev/mzcy/config/
├── ConfigLoader.java
├── ConfigManager.java
│
├── annotation/
│ ├── Config.java
│ ├── ConfigIgnore.java
│ └── ConfigName.java
│
├── exception/
│ ├── ConfigBackupException.java
│ ├── ConfigException.java
│ ├── ConfigInstantiationException.java
│ ├── ConfigLoadException.java
│ ├── ConfigParseException.java
│ ├── ConfigSaveException.java
│ ├── ConfigSerializationException.java
│ └── MissingConfigAnnotationException.java
│
└── internal/
├── io/
│ ├── AtomicConfigFileWriter.java
│ ├── ConfigFileReader.java
│ └── ConfigFileWriter.java
│
├── merge/
│ ├── ConfigMerger.java
│ └── JsonConfigMerger.java
│
├── metadata/
│ ├── ConfigMetadata.java
│ └── ConfigMetadataResolver.java
│
├── serializer/
│ ├── ConfigSerializer.java
│ ├── GsonConfigSerializer.java
│ │
│ └── gson/
│ ├── ConfigExclusionStrategy.java
│ └── ConfigFieldNamingStrategy.java
│
├── storage/
│ └── ConfigCache.java
│
└── util/
├── ConfigBackupUtil.java
└── ConfigReflectionUtil.java

Design Goals

Config is intentionally designed around a small public API.

Instead of:

Gsongson = ...;
Pathpath = ...;
Files.createDirectories(...);
Stringjson = ...;

the application should only need:

MyConfigconfig =
ConfigLoader.loadSafe(
MyConfig.class
);

Configuration classes remain normal Java classes and do not need to extend a base class or depend on a framework lifecycle.


License

MIT

About

A lightweight, easy-to-use JSON configuration library for Java.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages