Getting started
Invex.Extensions.Json provides utilities for flattening, unflattening, and updating
System.Text.Json node trees (JsonNode, JsonObject, JsonArray) using human-readable path
notation such as user:address:city.
Installation
dotnet add package Invex.Extensions.Json
Supported target frameworks: net10.0, net9.0, net8.0, and netstandard2.0
(the .NET Standard build references System.Text.Json as a package).
Namespaces
Everything lives in a single namespace:
using System.Text.Json.Nodes; // JsonNode, JsonObject, JsonArray
using Invex.Extensions.Json; // JsonExtensions, JsonUtil
The two API surfaces
The library exposes two complementary APIs. They solve the same problems with slightly different trade-offs — see Path notation for the full comparison.
1. JsonExtensions — string-valued, bracketed array indices
Best when the destination is a flat key/value store (configuration providers, environment variables, .properties-style files) where everything is a string anyway.
var json = JsonNode.Parse("""
{
"user": {
"name": "John",
"addresses": [ { "city": "New York", "zip": "10001" } ],
"tags": ["admin", "user"]
}
}
""")!;
// Flatten — array elements use [index] notation
IDictionary<string, string?> flat = JsonExtensions.Flatten(json);
// ["user:name"] = "John"
// ["user:addresses:[0]:city"] = "New York"
// ["user:addresses:[0]:zip"] = "10001"
// ["user:tags:[0]"] = "admin"
// ["user:tags:[1]"] = "user"
// Unflatten — rebuilds the hierarchy (all leaf values become JSON strings)
JsonObject rebuilt = JsonExtensions.Unflatten(flat);
2. JsonObject extension members — type-preserving, bare numeric indices
Best when you need lossless round-tripping: numbers stay numbers, booleans stay booleans.
var obj = JsonNode.Parse("""
{
"user": { "age": 42, "active": true, "tags": ["admin", "user"] }
}
""")!.AsObject();
// Flatten into another JsonObject — values keep their JSON types
JsonObject flatObj = obj.ToFlattenedJsonObject();
// {"user:age":42,"user:active":true,"user:tags:0":"admin","user:tags:1":"user"}
// Or flatten into a Dictionary<string, string?>
Dictionary<string, string?> flatDict = obj.ToFlattenedDictionary();
// ["user:age"] = "42", ["user:active"] = "true", ...
// Rebuild the hierarchy (numeric segments become array indices)
JsonObject roundTrip = flatObj.ToUnflattenedJsonObject();
// Quick check: is this object already flat?
bool nested = obj.HasNestedObjects(); // true
Updating values
Both single and batch replacement modify the JsonObject in place and are deliberately
conservative — they never create intermediate containers:
var obj = JsonNode.Parse("""
{ "name": "John", "user": { "address": { "city": "NYC" } }, "users": [ { "name": "A" } ] }
""")!.AsObject();
// Single value
obj.ReplaceValue("user:address:city", "LA");
// Batch — supports stepping into arrays with bare numeric segments
obj.ReplaceValues(new Dictionary<string, string?>
{
["name"] = "Jane",
["users:0:name"] = "Alpha",
});
See Replacing values for the full semantics, including fallback behavior when a path doesn't exist.
Custom separators
Every method accepts a separator parameter (default ":"). Separators may be longer than
one character:
var flat = JsonExtensions.Flatten(json, separator: "__");
// ["user__name"] = "John", ...
var rebuilt = JsonExtensions.Unflatten(flat, separator: "__");
Important
Choose a separator that never occurs in your property names — otherwise unflattening will split those names incorrectly. Always use the same separator for flattening and unflattening.
Next steps
- Path notation — how paths are formed and parsed
- Flattening & unflattening —
JsonExtensionsin depth - JsonObject extension members — the type-preserving API
- Replacing values — update semantics and fallbacks