Config Manager
A Dart configuration manager package that loads JSON configuration files and provides path-based access to configuration values.
Features
- Load configuration from JSON files, raw JSON strings, or in-memory maps
- Access nested values via an explicit key path (
List<String>), e.g.['database', 'host']— a single top-level key is a one-element list, e.g.['appName']; an empty list[]addresses the whole configuration map - List support with index access (e.g.
['features', '0']) - Sector-based configuration, so multiple independent configs can coexist
- Check whether a sector is loaded (
isLoaded) or whether a set of key paths is present (containsKeys) - Skip reloading an already-loaded sector with
force: false setrejects writing a non-null value for a key whose type is already known and differs from it, throwingConfigTypeMismatchException- Optional type schema (
setSchema/typeOf): declare expected types for a sector by hand, or letsetand every load call build them up on their own from the very first write/load of a key — every subsequentset/loadFromJson/loadFromString/loadFromMapcall is validated against it; adynamicschema leaf opts a key out of checking entirely - Singleton-based configuration access
- Mixin-based configuration extension for classes
getOrDefault<T>/getDurationSecondsfall back to a default configuration tree when a key isn't present in what's loadedlookupPath/deepMergeMapstop-level utilities for path-walking and deep-merging plainMap<String, dynamic>trees
Usage
import 'package:config_manager/config_manager.dart';
void main() {
final config = ConfigManagerSingleton();
config.loadFromJson('config.json');
print(config.get(['appName']));
print(config.get(['database', 'host']));
print(config[['database', 'credentials', 'user']]);
// get<T> is generic: T defaults to dynamic, but can be specified to get
// a typed value back without an explicit cast at the call site. Since
// JSON carries no type information, a mismatched T throws a TypeError
// at call time rather than being caught at compile time.
final appName = config.get<String>(['appName']);
final port = config.get<int>(['database', 'port']);
// set and every load call check a non-null value against whatever type
// is already known for that key, and record it if not: loading
// config.json above already taught the schema that database.port is an
// int, so this succeeds...
config.set(['database', 'port'], 5433); // ok: int -> int
// ...and this would throw, without ever calling setSchema by hand:
// config.set(['database', 'port'], 'not a port');
// Optional: setSchema declares types up front, e.g. for a key you want
// checked before it's ever written or loaded.
config.setSchema({
'database': {'host': String},
});
print(config.typeOf(['database', 'port'])); // int, learned from the load
// config.loadFromJson('bad_config.json'); // throws if a type disagrees
// A schema leaf of `dynamic` opts a key out of checking entirely, and
// stays that way even after future writes/loads.
config.setSchema({'appName': dynamic});
config.set(['appName'], 42); // fine, no longer locked to String
print(config.isLoaded());
print(config.containsKeys([
['appName'],
['database', 'host'],
]));
// force: false is a no-op when the sector is already loaded.
config.loadFromJson('config.json', force: false);
// A key path is always an explicit list of segments, so a literal "."
// inside a key name is never ambiguous with nesting.
config.set(['database.host'], 'literal-value');
print(config.get(['database.host']));
// getOrDefault falls back to a default tree for a partially loaded
// config, e.g. one loaded via loadFromString/loadFromMap that omits a
// key. getDurationSeconds does the same, reading the value as seconds
// and converting it to a Duration.
const defaults = {
'timeouts': {'connect': 5},
};
final connectTimeout =
config.getDurationSeconds(['timeouts', 'connect'], defaults);
print(connectTimeout); // 0:00:05.000000, even if "timeouts" was never loaded
// lookupPath/deepMergeMaps are the plain-Map building blocks behind
// getOrDefault/getDurationSeconds; useful directly when working with
// configuration trees outside of a loaded sector.
print(lookupPath(defaults, ['timeouts', 'connect'])); // 5
print(deepMergeMaps(defaults, {'timeouts': {'connect': 10}}));
}
Examples
The example/ folder has one focused file per topic, each runnable on its
own (e.g. dart run example/sectors_example.dart). example/main.dart runs
all of them in sequence.
basic_usage.dart— loading a JSON file, reading valueslists_and_paths.dart— lists, casting, index accesswrites_and_edge_cases.dart— writing nested values, missing keyssectors_example.dart— independent per-sector configurationis_loaded_and_contains_keys.dart—isLoadedandcontainsKeysforce_reload.dart—force: falseno-op reloadnested_vs_flat_keys.dart— nested paths vs. a literal dotted keymixin_extension.dart—ConfigExtensionmixincustom_extension.dart— a customIConfigExtensionimplementation
License
MIT
Libraries
- config_manager
- A JSON-based configuration manager for Dart applications.