Invex.Extensions.Json is a small, allocation-conscious library for flattening,
unflattening, and updating System.Text.Json node trees
(JsonNode, JsonObject, and JsonArray) with readable paths such as
user:address:city. It is useful for configuration and environment-variable
adapters, ETL pipelines, JSON diffing or patching, and exchanging JSON with flat
key/value stores.
- Flatten any node. Turn any
JsonNodeinto path/value pairs. - Rebuild hierarchies. Recreate a
JsonObjectfrom flattened pairs. - Update in place. Replace one or many values in an existing
JsonObject, including array elements. - Type preservation. Keep JSON value types when flattening to a
JsonObject. - Configurable separators. Use any separator, including multi-character ones such as
"__". - Lean hot paths. No reflection or regular expressions when flattening and unflattening.
dotnet add package Invex.Extensions.JsonThe package targets net10.0 and netstandard2.0, and
exposes the Invex.Extensions.Json namespace. The netstandard2.0 target
includes a System.Text.Json package dependency.
using System.Text.Json.Nodes;
using Invex.Extensions.Json;
var json = JsonNode.Parse("""
{
"user": {
"name": "John",
"tags": ["admin", "user"]
}
}
""")!;
// String-valued flattening uses bracketed array indices.
IDictionary<string, string?> flat = JsonExtensions.Flatten(json);
// user:name = "John"
// user:tags:[0] = "admin"
// user:tags:[1] = "user"
// The matching operation rebuilds the hierarchy.
JsonObject rebuilt = JsonExtensions.Unflatten(flat);
// Replacements mutate the existing object and use bare numeric indices.
rebuilt.ReplaceValues(new Dictionary<string, string?>
{
["user:name"] = "Jane",
["user:tags:1"] = "editor",
});The library has two complementary path and value models. Pair operations from the same column when round-tripping.
JsonExtensions |
JsonObject extension members |
|
|---|---|---|
| Flattening input | Any JsonNode |
JsonObject |
| Array path syntax | Bracketed: users:[0]:name |
Bare numeric: users:0:name |
| Flattened values | string? |
Original JSON nodes, or string? for the dictionary method |
| Unflattened arrays | Append order; no sparse padding | Numeric indices; sparse indices padded with null |
| Best suited to | Configuration and string key/value stores | Lossless JSON round-tripping |
See Introduction for when to use each surface.
JsonExtensions.Flatten returns string values (JSON null becomes a null value),
and Unflatten rebuilds a JsonObject whose leaves are all JSON strings. Use
the same separator for both halves of a round trip:
var flat = JsonExtensions.Flatten(json, separator: "__");
var rebuilt = JsonExtensions.Unflatten(flat, separator: "__");Property names are not escaped, so pick a separator that never occurs in them. See Flattening and Unflattening and Path Notation.
var source = JsonNode.Parse(
"""{ "a": { "number": 42, "enabled": true, "items": [null, "x"] } }""")!
.AsObject();
JsonObject flatObject = source.ToFlattenedJsonObject();
// a:number = 42, a:enabled = true, a:items:0 = null, a:items:1 = "x"
JsonObject roundTrip = flatObject.ToUnflattenedJsonObject();
Dictionary<string, string?> flatDictionary = source.ToFlattenedDictionary();
bool hasNestedValues = source.HasNestedObjects();Leaf nodes are deep-cloned in both directions, so results never share mutable nodes with their source. See JsonObject Extension Members.
ReplaceValue and ReplaceValues modify the supplied JsonObject and return
the same instance. Replacement values are stored as JSON strings (or JSON null),
and no intermediate objects or arrays are ever created. ReplaceValues can step
into existing arrays with bare numeric segments such as users:0:name; a
bracketed [0] is treated as a literal property name.
var obj = JsonNode.Parse("""
{ "user": { "address": { "city": "NYC" } }, "users": [ { "name": "A" } ] }
""")!.AsObject();
obj.ReplaceValue("user:address:city", "LA")
.ReplaceValues(new Dictionary<string, string?> { ["users:0:name"] = "Alpha" });See Replacing Values for the complete traversal and fallback rules.
Full documentation: https://invex-games.github.io/json-extensions/
| Guide | Contents |
|---|---|
| Introduction | What the library is for, the two API surfaces, and design principles |
| Getting Started | Installation, namespaces, and guided examples |
| Path Notation | Array conventions, separators, values, and edge cases |
| Flattening and Unflattening | JsonExtensions.Flatten and Unflatten |
| JsonObject Extension Members | Type-preserving flattening and reconstruction |
| Replacing Values | In-place updates, arrays, and fallbacks |
| API Reference | Generated reference for every public type |
The repository builds with the .NET SDK selected by global.json. From the
repository root:
dotnet build Invex.Extensions.Json.slnx
dotnet test Invex.Extensions.Json.slnxThe documentation site is built with DocFX
from the conceptual pages in docs/ and the XML doc comments in the source.
Build or preview it through the Atom build:
dotnet run --project _atom -- BuildDocs
dotnet run --project _atom -- ServeDocs(atom BuildDocs and atom ServeDocs when the Atom tool is installed.) To run
DocFX directly instead:
dotnet tool install -g docfx
docfx docfx.json --serveThen browse to http://localhost:8080.
Licensed under the MIT License.