JSON Configuration
Builder reference
| Method | Effect |
|---|---|
OverrideDirectory(string) | Directory the instance’s files resolve against. Defaults to JsonConfigManager.DefaultDirectory ("./config"). |
Register<T>(string fileName, bool encrypted = false) | Associates T with an existing JSON file. |
OverrideJsonOptions(JsonSerializerOptions) | Replaces the default serializer options entirely, rather than merging into them. |
ApplyConverterFor<T>(JsonConverter<T>) | Swaps in a custom converter for T. See Custom converters. |
ApplyEncryptionKey(Func<string>) | Supplies the passphrase for encrypted: true registrations. See Encryption. |
EnableHotReload() | Watches the directory for file changes and invalidates affected cache entries. See Hot Reload. |
EnsureIsolatedGets() | Returns a fresh deserialize from Get<T> on every call instead of the cached reference. See Isolated gets. |
ApplyValidatorFor<T>(Func<T, bool>, string reason) | Attaches a build-time (and Put-time) validator. See Validation. |
Build() | Validates every registration and returns the live instance, or throws. |
Registering a config type
Register<T>(fileName, encrypted = false) associates a plain C# type with a JSON file in the instance’s directory:
var configMgr = JsonConfigManager.BuildInstance("default")
.OverrideDirectory(@"C:\data\myapp\config") // defaults to "./config"
.ApplyEncryptionKey(() => "at-least-twelve-characters")
.Register<AppSettings>("app-settings.json")
.Register<ConnectionStrings>("connection-strings.aes", encrypted: true)
.Build();Create<T>: app-settings.json must already exist under the configured directory by Build() time. Authoring the initial file (plaintext, or pre-encrypted via the CLI Tool or ConfigEncryptor) is the caller’s responsibility, outside the manager.Registering the same type twice on one instance throws InvalidOperationException. Registering encrypted: true has its own build-time requirement; see Encryption.
File naming and directory scoping
An instance is scoped to exactly one flat directory. fileName must be a plain file name, with no path separators, and not rooted:
.Register<T>("../secrets.json") // ArgumentException: escapes the configured directory
.Register<T>(@"C:\secrets.json") // ArgumentException: rooted
.Register<T>(@"sub\config.json") // ArgumentException: contains a path separatorThis is enforced even when the target file genuinely exists: a subdirectory fileName is rejected regardless. To organize configs into folders, build a separate named instance per subdirectory, each with its own OverrideDirectory:
var connStrMgr = JsonConfigManager.BuildInstance("conn-strings")
.OverrideDirectory(@"C:\data\myapp\config\conn-strings")
.Register<ConnectionStrings>("connection-strings.json")
.Build();Reading
AppSettings settings = configMgr.Get<AppSettings>();Get<T> deserializes and caches on first access, unless T had a validator applied and was already pre-cached during Build() (see Validation). Either way, later calls return the cached instance. Pass refresh: true to force a re-read from disk and repopulate the cache:
AppSettings latest = configMgr.Get<AppSettings>(refresh: true);Get<T> throws InvalidOperationException if T was never registered on the instance.
Isolated gets
By default, Get<T> returns a reference to the cached object, not a copy. A caller could mutate the instance via the returned reference, a reference-aliasing bug that bypasses the API entirely: no Put call is needed to cause or observe it.
EnsureIsolatedGets() on the builder closes this instance-wide, with no per-type opt-in: instead of caching the deserialized object, the instance caches the raw JSON string and re-deserializes a fresh instance from it on every Get<T> call.
var configMgr = JsonConfigManager.BuildInstance("default")
.EnsureIsolatedGets()
.Register<AppSettings>("app-settings.json")
.Build();The tradeoff is a deserialize on every Get<T> instead of an O(1) cache hit. Leave it off when callers are trusted not to mutate what Get<T> returns and the cache hit matters; turn it on when an instance is shared across callers or teams where an accidental mutation is a more expensive failure than the extra deserialize.
Writing
AppSettings settings = configMgr.Get<AppSettings>();
settings.TimeoutSeconds = 60;
bool written = configMgr.Put(settings);Put<T> serializes and writes atomically (write to a temp sibling file, then move into place) and invalidates the cache for that type. It also guards against a lost update: if Put has previously tracked a version for T (from an earlier Get or Put on this instance, or from build-time validation) and the on-disk content has since changed (a concurrent process, a manual edit), Put returns false without writing. The documented recovery is to refresh and retry:
if (!configMgr.Put(settings))
{
settings = configMgr.Get<AppSettings>(refresh: true); // pick up the external change
settings.TimeoutSeconds = 60; // reapply
configMgr.Put(settings); // retry
}If no version has been tracked yet for T on this instance, Put writes unconditionally: the concurrency check only applies once this instance has actually observed the file’s content at least once.
Custom converters
ApplyConverterFor<T> swaps in a JsonConverter<T> for the instance’s serializer options. For most T this is optional, but it’s required for a T that System.Text.Json can’t instantiate on its own, most commonly an interface: registering IAccountSettings without a matching converter throws NotSupportedException at Build() (or at the first Get<T> if the type has no validator forcing eager deserialization).
JsonConfigManager.BuildInstance("default")
.ApplyConverterFor<IAccountSettings>(new AccountSettingsConverter())
.Register<IAccountSettings>("account-settings.json")
.Build();It replaces an existing converter registered specifically for T if one is present, rather than matching anything whose CanConvert(T) returns true. A broad converter like JsonStringEnumConverter for unrelated types is left alone.
Default serializer options
Unless overridden with OverrideJsonOptions, JsonConfigManager uses:
| Option | Value |
|---|---|
WriteIndented | true |
MaxDepth | 512 |
DefaultBufferSize | 8192 |
IncludeFields | false |
IgnoreReadOnlyFields | true |
AllowTrailingCommas | true |
PropertyNameCaseInsensitive | true |
ReadCommentHandling | Skip |
| Converters | JsonStringEnumConverter pre-registered |
JsonConfigManagerBuilder.DefaultJsonOptions is a static property that returns a fresh JsonSerializerOptions populated with this exact table, so a single default can be tweaked without hand-reconstructing the rest:
var options = JsonConfigManagerBuilder.DefaultJsonOptions;
options.PropertyNameCaseInsensitive = false;
JsonConfigManager.BuildInstance("default")
.OverrideJsonOptions(options)
.Register<AppSettings>("app-settings.json")
.Build();Each call returns a new instance, not a shared one, so mutating the result never affects other builders.
DefaultJsonOptions, clear Converters to drop JsonStringEnumConverter, and pass the result to OverrideJsonOptions; with no enum converter present, System.Text.Json falls back to the underlying integer value.Cache control
ClearCached(Type) and ClearAllCached() drop cached instances without touching the underlying files: the next Get<T> re-reads from disk. GetFileName(Type) returns the file name a type was registered under, or null if it wasn’t registered.
Next steps
- Encryption: encrypted registrations,
ConfigEncryptor - Validation:
ApplyValidatorFor<T>and build-time validation - Hot Reload: automatic cache invalidation on external file changes