Original Code made by ElectronWill NightConfig is a powerful yet easy-to-use java configuration library, written in Java 21.
It supports the following formats:
- Quick Start
- Features
- Basic Configuration Operations
- FileConfig: File-Based Configuration
- CommentedConfig: Configuration with Comments
- ConfigSpec: Validation & Correction
- Serde System: Advanced Serialization/Deserialization
- TypeAdapter: Custom Generic Type Handling
- SerdeContext: Type-Aware Config Operations
- Concurrent Configurations
- Modules and Dependencies
- Running the Examples
- Project Building
// Simple builder:
FileConfig conf = FileConfig.of("the/file/config.toml");
// Advanced builder, default resource, autosave and much more (-> cf the wiki)
CommentedFileConfig config = CommentedFileConfig.builder("myConfig.toml")
.defaultResource("defaultConfig.toml")
.autosave()
.build();
config.load(); // This actually reads the config
String name = config.get("username"); // Generic return type!
List<String> names = config.get("users_list"); // Generic return type!
long id = config.getLong("account.id"); // Compound path: key "id" in subconfig "account"
int points = config.getIntOrElse("account.score", defaultScore); // Default value
config.set("account.score", points * 2);
String comment = config.getComment("user");
// NightConfig saves the config's comments (for TOML, HOCON and YAML)
// config.save(); not needed here thanks to autosave()
config.close(); // Close the FileConfig once you're done with it :)The Config interface provides core operations for reading and writing configuration values.
// In-memory config (not thread-safe)
Config config = Config.inMemory();
// In-memory config with concurrent map (basic thread-safety for Map operations)
Config concurrentConfig = Config.inMemoryConcurrent();
// From specific format
Config tomlConfig = TomlFormat.instance().createConfig();// Get with automatic type inference
String value = config.get("key");
List<String> list = config.get("myList");
// Get with type-specific methods
int intValue = config.getInt("number");
long longValue = config.getLong("bigNumber");
double doubleValue = config.getDouble("decimal");
boolean boolValue = config.getBoolOrElse("flag", false);
// Get with default values
String name = config.getOrElse("username", "anonymous");
int score = config.getIntOrElse("score", 0);
// Nested paths (dot notation)
String nested = config.get("database.connection.host");// Set values
config.set("key", "value");
config.set("nested.path.key", 123);
// Add only if not exists
boolean added = config.add("key", "value");
// Remove values
Object removed = config.remove("key");
// Clear all values
config.clear();
// Bulk operations
config.putAll(anotherConfig); // Overwrites existing
config.addAll(anotherConfig); // Only adds missing entriesFileConfig ties a configuration to a file, with automatic format detection and many builder options.
// Auto-detect format from extension
FileConfig config = FileConfig.of("config.toml");
config.load();
// Use values...
String value = config.get("key");
config.set("key", "newValue");
// Save and close
config.save();
config.close();FileConfig config = FileConfig.builder("config.toml")
.charset(StandardCharsets.UTF_8) // Character encoding
.writingMode(WritingMode.REPLACE_ATOMIC) // Atomic file writes
.parsingMode(ParsingMode.REPLACE) // How to merge loaded data
.onFileNotFound(FileNotFoundAction.CREATE_EMPTY) // Action when file missing
.build();Copy a default config from your JAR resources if the file doesn't exist:
FileConfig config = FileConfig.builder("config.toml")
.defaultResource("defaultConfig.toml") // Path in resources
.build();
config.load(); // Creates file from resource if not foundAutomatically save the config whenever it's modified:
FileConfig config = FileConfig.builder("config.json")
.autosave()
.build();
config.load();
config.set("value", 123);
// File is saved automatically!
config.close();Automatically reload the config when the file changes on disk:
FileConfig config = FileConfig.builder("config.json")
.autoreload()
.build();
config.load();
// Config automatically reloads when file is modified externally
// Reloading is throttled to prevent excessive reads
config.close();// Synchronous: save() blocks until complete
FileConfig syncConfig = FileConfig.builder("config.toml")
.sync()
.build();
// Asynchronous: save() returns immediately
FileConfig asyncConfig = FileConfig.builder("config.toml")
.async()
.build();
// Async with custom debouncing
FileConfig debouncedConfig = FileConfig.builder("config.toml")
.asyncWithDebouncing(Duration.ofMillis(500))
.build();Group multiple operations to avoid triggering autosave on each change:
config.bulkUpdate(c -> {
c.set("key1", "value1");
c.set("key2", "value2");
c.set("key3", "value3");
// Autosave triggers only once at the end
});For formats that support comments (TOML, YAML, HOCON), use CommentedConfig:
CommentedFileConfig config = CommentedFileConfig.builder("config.toml").build();
config.load();
// Set values with comments
config.set("database.host", "localhost");
config.setComment("database.host", "The database server hostname");
// Get comments
String comment = config.getComment("database.host");
// Remove comments
config.removeComment("database.host");
config.save();
config.close();ConfigSpec defines constraints for configuration values and can automatically correct invalid configs.
ConfigSpec spec = new ConfigSpec();
// Define with default values
spec.define("username", "anonymous");
// Define numeric ranges (inclusive)
spec.defineInRange("port", 8080, 1, 65535);
spec.defineInRange("volume", 0.5, 0.0, 1.0);
// Define allowed values
spec.defineInList("difficulty", "normal", Arrays.asList("easy", "normal", "hard"));
// Define with enum class
spec.defineOfClass("logLevel", LogLevel.INFO, LogLevel.class);Config config = Config.inMemory();
config.set("port", 99999); // Invalid: out of range
config.set("extra_key", "x"); // Not in spec
// Check if config is valid
boolean isCorrect = spec.isCorrect(config);
// Auto-correct the config
spec.correct(config);
// - port is reset to default (8080)
// - extra_key is removed
// - missing entries are added with defaultsThe serde package provides a modern, flexible serialization system with ObjectSerializer and ObjectDeserializer.
// Serialization: Object -> Config
ObjectSerializer serializer = ObjectSerializer.standard();
Config config = serializer.serializeFields(myObject, Config::inMemory);
// Deserialization: Config -> Object
ObjectDeserializer deserializer = ObjectDeserializer.standard();
MyClass object = deserializer.deserializeFields(config, MyClass::new);
// Deserialize to a Record (Java 17+)
MyRecord record = deserializer.deserializeToRecord(config, MyRecord.class);class User {
@SerdeKey("user_id")
private String id; // Serializes to "user_id" in config
}class User {
@SerdeSkip // Always skip this field
private transient Object cache;
@SerdeSkip(value = SkipIf.IS_NULL) // Skip only if null
private String optional;
@SerdeSkip(value = SkipIf.IS_EMPTY) // Skip if empty collection/string
private List<String> items;
@SerdeSkip(value = SkipIf.CUSTOM, customCheck = "shouldSkip")
private String conditional;
private boolean shouldSkip(Object value) {
return value == null || value.toString().isEmpty();
}
}Fine-grained control for skip behavior during specific phases:
class Config {
@SerdeSkipDeserializingIf(SkipIf.IS_NULL) // Skip when deserializing if config value is null
private String value;
@SerdeSkipSerializingIf(SkipIf.IS_EMPTY) // Skip when serializing if field is empty
private List<String> items;
}class MyConfig {
// Default from a method
@SerdeDefault(provider = "defaultServers")
private List<String> servers;
List<String> defaultServers() {
return Arrays.asList("localhost");
}
// Default when null
@SerdeDefault(provider = "getDefaultName", whenValue = {WhenValue.IS_NULL})
private String name;
// Default when missing, null, or empty
@SerdeDefault(
provider = "getDefaultItems",
whenValue = {WhenValue.IS_MISSING, WhenValue.IS_NULL, WhenValue.IS_EMPTY}
)
private List<String> items;
// Different defaults for serializing vs deserializing
@SerdeDefault(provider = "defaultForDeserializing", phase = SerdePhase.DESERIALIZING)
@SerdeDefault(provider = "defaultForSerializing", phase = SerdePhase.SERIALIZING)
private String bidirectional;
}class ServerConfig {
@SerdeComment("The server hostname or IP address")
private String host;
@SerdeComment("First line of comment")
@SerdeComment("Second line of comment")
private int port;
}Throw an exception if a field doesn't match specified conditions:
class User {
@SerdeAssert(AssertThat.NOT_NULL) // Throws if null
private String username;
@SerdeAssert(AssertThat.NOT_EMPTY) // Throws if empty
private List<String> roles;
@SerdeAssert(value = AssertThat.CUSTOM, customCheck = "isValidEmail")
private String email;
private boolean isValidEmail(String value) {
return value != null && value.contains("@");
}
}Assertions can be scoped to specific phases:
@SerdeAssert(value = AssertThat.NOT_NULL, phase = SerdePhase.DESERIALIZING)
private String requiredOnLoad;Consolidate multiple serde annotations into a single annotation:
class Player {
@SerdeConfig(
key = "player_name",
comments = @SerdeComment("The player's display name"),
asserts = @SerdeAssert(AssertThat.NOT_NULL),
defaults = @SerdeDefault(provider = "defaultName")
)
private String name;
String defaultName() {
return "Anonymous";
}
@SerdeConfig(
skip = @SerdeSkip(value = SkipIf.IS_NULL),
skipSerializingIf = @SerdeSkipSerializingIf(SkipIf.IS_EMPTY)
)
private List<String> achievements;
}Available @SerdeConfig options:
key- Custom config key namecomments- Array of@SerdeCommentasserts- Array of@SerdeAssertdefaults- Array of@SerdeDefaultskip- Array of@SerdeSkipskipDeserializingIf- Array of@SerdeSkipDeserializingIfskipSerializingIf- Array of@SerdeSkipSerializingIf
Transform field names automatically during serialization:
ObjectSerializer serializer = ObjectSerializer.builder()
.withNamingStrategy(NamingStrategy.SNAKE_CASE)
.build();
ObjectDeserializer deserializer = ObjectDeserializer.builder()
.withNamingStrategy(NamingStrategy.SNAKE_CASE)
.build();Available strategies:
IDENTITY- No transformation (default)SNAKE_CASE-userName→user_nameKEBAB_CASE-userName→user-nameCAMEL_CASE-user_name→userNamePASCAL_CASE-userName→UserName
TypeAdapter allows custom serialization/deserialization of generic types while preserving type information.
// Generic wrapper class
class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T getValue() { return value; }
}
// TypeAdapter implementation
class BoxTypeAdapter<T> implements TypeAdapter<Box<T>, Object> {
@Override
public boolean canHandle(Type type) {
if (type instanceof ParameterizedType pt) {
return pt.getRawType() == Box.class;
}
return type == Box.class;
}
@Override
public Object serialize(Box<T> value, Type type, SerializerContext ctx) {
return ctx.serializeValue(value.getValue());
}
@Override
public Box<T> deserialize(Object value, Type type, DeserializerContext ctx) {
Type valueType = Object.class;
if (type instanceof ParameterizedType pt) {
Type[] typeArgs = pt.getActualTypeArguments();
if (typeArgs.length > 0) {
valueType = typeArgs[0];
}
}
T innerValue = (T) ctx.deserializeValue(value, new TypeConstraint(valueType));
return new Box<>(innerValue);
}
}
// Usage
ObjectSerializer serializer = ObjectSerializer.builder()
.withTypeAdapter(new BoxTypeAdapter<>())
.build();
ObjectDeserializer deserializer = ObjectDeserializer.builder()
.withTypeAdapter(new BoxTypeAdapter<>())
.build();SerdeContext allows you to use TypeAdapters directly with Config.get() and Config.set() methods, enabling automatic type conversion without manual handling.
// Create context with TypeAdapters
SerdeContext ctx = SerdeContext.builder()
.withTypeAdapter(new ResourceLocationTypeAdapter())
.build();
// Attach to any Config
Config config = Config.inMemory();
config.setSerdeContext(ctx);
// Use setTyped/getTyped for automatic conversion
ResourceLocation loc = new ResourceLocation("minecraft", "stone");
config.setTyped("block", loc); // Automatically serializes to String
ResourceLocation retrieved = config.getTyped("block", ResourceLocation.class);
// Automatically deserializes back to ResourceLocationResourceLocation defaultLoc = new ResourceLocation("minecraft", "air");
ResourceLocation result = config.getTypedOrElse("missing", ResourceLocation.class, defaultLoc);You can register TypeAdapters at runtime:
SerdeContext ctx = SerdeContext.builder().build();
config.setSerdeContext(ctx);
// Register later
ctx.registerTypeAdapter(new MyTypeAdapter());
// Now it works
config.setTyped("key", myCustomObject);Works seamlessly with FileConfig and other Config implementations:
FileConfig config = FileConfig.of("config.toml");
config.load();
SerdeContext ctx = SerdeContext.builder()
.withTypeAdapter(new ResourceLocationTypeAdapter())
.build();
config.setSerdeContext(ctx);
// Save custom types
config.setTyped("spawn.location", new ResourceLocation("minecraft", "plains"));
config.save();
config.close();For multi-threaded applications, use thread-safe configurations from the concurrent package.
SynchronizedConfig- Uses synchronized blocksStampedConfig- UsesStampedLockfor better performance
The key feature is atomic bulk operations:
ConcurrentConfig config = new SynchronizedConfig();
// Atomic bulk update
List<String> result = config.bulkUpdate(c -> {
List<String> players = c.get("players");
players.add("NewPlayer");
players.remove("BadPlayer");
c.set("players", players);
return players;
});
// Atomic bulk read
String data = config.bulkRead(c -> {
String name = c.get("name");
int score = c.getInt("score");
return name + ": " + score;
});Important: In bulk operations, only use the config view provided to your function (
c), not the original config reference.
FileConfig uses ConcurrentConfig internally and supports the same bulk operations:
FileConfig config = FileConfig.builder("config.toml").build();
config.bulkUpdate(c -> {
c.set("key1", "value1");
c.set("key2", "value2");
});NightConfig is modular. Add only what you need:
| Module | Description | Dependency |
|---|---|---|
core |
Core API (Config, FileConfig, etc.) | Always required |
toml |
TOML format support | re.neotamia.night-config:toml |
json |
JSON format support | re.neotamia.night-config:json |
yaml |
YAML format support | re.neotamia.night-config:yaml |
hocon |
HOCON format support | re.neotamia.night-config:hocon |
Add the NeoTamia repository to your build configuration:
repositories {
maven {
url = uri("https://repo.neotamia.re/releases")
}
// For snapshots:
// maven {
// url = uri("https://repo.neotamia.re/snapshots")
// }
}
dependencies {
implementation("re.neotamia.night-config:core:VERSION")
implementation("re.neotamia.night-config:toml:VERSION")
}repositories {
maven { url 'https://repo.neotamia.re/releases' }
// For snapshots:
// maven { url 'https://repo.neotamia.re/snapshots' }
}
dependencies {
implementation 're.neotamia.night-config:core:VERSION'
implementation 're.neotamia.night-config:toml:VERSION'
}<repositories>
<repository>
<id>neotamia-releases</id>
<url>https://repo.neotamia.re/releases</url>
</repository>
<!-- For snapshots:
<repository>
<id>neotamia-snapshots</id>
<url>https://repo.neotamia.re/snapshots</url>
</repository>
-->
</repositories>
<dependencies>
<dependency>
<groupId>re.neotamia.night-config</groupId>
<artifactId>core</artifactId>
<version>VERSION</version>
</dependency>
<dependency>
<groupId>re.neotamia.night-config</groupId>
<artifactId>toml</artifactId>
<version>VERSION</version>
</dependency>
</dependencies>Each file in examples/src/main/java has a main function and shows how to use NightConfig for many different use cases.
To run an example:
- Clone this repository.
cdto it- Run
./gradlew examples:run -PmainClass=${CLASS}by replacing${CLASS}with the example of your choice.
For example, to run FileConfigExample.java:
./gradlew examples:run -PmainClass=FileConfigExampleAvailable examples:
FileConfigExample- Basic file configuration usageAutosaveExample- Automatic saving on modificationAutoreloadExample- Automatic reloading on file changeConfigSpecExample- Configuration validation and correctionTypeAdapterExample- Custom generic type handlingTypeAdapterWithListExample- TypeAdapter with collectionsSerdeContextExample- Type-aware get/set with TypeAdaptersCommentedConfigExample- Working with comments
NightConfig is built with Gradle. The project is divided in several modules, the "core" module plus one module per supported configuration format. Please read the wiki for more information.
Older versions of Android (before Android Oreo) didn't provide the packages java.util.function and java.nio.file, which NightConfig heavily uses.
To attempt to mitigate these issues, I made a special version of each modules, suffixed with _android, that you could use instead of the regular modules.
These old _android modules are deprecated and will no longer receive updates.
The maintainance burden of these modules is not worth it, and these versions of Android have reached end of life since several years already.
