Class JsonExtensions
- Namespace
- Invex.Extensions.Json
- Assembly
- Invex.Extensions.Json.dll
Extension methods for working with System.Text.Json nodes using a human-readable path notation.
Path conventions:
-
Object properties are separated by a configurable separator, colon by default
(e.g.,
"user:address:city"). -
Arrays are addressed with bracketed indices (e.g.,
"users:[0]:name") in flattened/unflattened keys. -
For in-place replacement via
ReplaceValues(JsonObject, Dictionary<string, string?>, string), array steps use
bare numeric segments instead (e.g.,
"users:0:name").
These helpers are allocation-conscious and designed for clarity when manipulating JSON in config and ETL scenarios.
public static class JsonExtensions
- Inheritance
-
objectJsonExtensions
Methods
Flatten(JsonNode, string)
Flattens a hierarchical JSON structure into a flat dictionary of path/value pairs. Nested objects are represented using separator-joined paths, and array elements use bracket notation with zero-based indices.
public static IDictionary<string, string?> Flatten(JsonNode node, string separator = ":")
Parameters
nodeJsonNodeThe JSON node to flatten. Can be a System.Text.Json.Nodes.JsonObject, System.Text.Json.Nodes.JsonArray, or primitive value.
separatorstringThe string used to join path segments. May be more than one character. Defaults to
":".
Returns
- IDictionary<string, string>
A dictionary mapping each leaf path to its string representation:
-
Keys use
separatorbetween object property segments and[index]notation for array elements (e.g.,"user:tags:[0]"). -
Values are the string representation of the JSON values (
nullfor JSON null values), so the original JSON value kinds are not preserved.
The source node is not modified.
-
Keys use
Examples
var json = JsonNode.Parse("""{ "user": { "name": "John", "tags": ["admin", "user"] } }""");
var flattened = JsonExtensions.Flatten(json);
// { ["user:name"] = "John", ["user:tags:[0]"] = "admin", ["user:tags:[1]"] = "user" }
Remarks
Empty nested objects and arrays produce no entries in the output. If node is
itself a primitive value, the result contains a single entry with an empty-string key.
The output can be passed to Unflatten(IDictionary<string, string?>, string) to rebuild the structure (all values become JSON strings).
Exceptions
- ArgumentNullException
Thrown when
nodeis null.
ReplaceValue(JsonObject, string, string?, string)
Replaces the value at a specified path within a JSON object, modifying the object in place.
public static JsonObject ReplaceValue(this JsonObject root, string path, string? value, string separator = ":")
Parameters
rootJsonObjectThe JSON object to modify.
pathstringThe property path to replace. Use
separator-joined segments for nested objects (e.g.,"user:address:city"). Array indices are not supported by this method. If the path contains noseparator, it is treated as a simple, root-level property name. An empty path is ignored and the object is returned unchanged.valuestringThe new value to assign at the specified path. Use
nullfor JSON null.separatorstringThe string used to split path segments. Defaults to
":".
Returns
- JsonObject
The same System.Text.Json.Nodes.JsonObject instance passed in, after modification:
-
For simple paths (no
separator), the property is only replaced if it already exists; missing properties are not added. -
For
separator-joined paths, only existing intermediate System.Text.Json.Nodes.JsonObject nodes are traversed; missing segments are not created. The final segment is then set on the deepest object reached (potentially the root), adding the property there if it does not exist.
-
For simple paths (no
Examples
var obj = JsonNode.Parse("""{ "user": { "name": "John" } }""").AsObject();
obj.ReplaceValue("user:name", "Jane");
// {"user":{"name":"Jane"}}
obj.ReplaceValue("unknown", "x");
// Unchanged: simple paths never add missing root-level properties.
Remarks
This method is intentionally conservative about creating structure. If you need batch updates and support for stepping into arrays, see ReplaceValues(JsonObject, Dictionary<string, string?>, string).
All replacement values are stored as JSON strings (or JSON null); existing value kinds such as numbers or booleans are not preserved at the target path.
If root is null, a System.NullReferenceException is thrown when the
object is first dereferenced, per extension method invocation semantics.
Exceptions
- ArgumentNullException
Thrown when
pathis null.
ReplaceValues(JsonObject, Dictionary<string, string?>, string)
Applies multiple replacements to a JSON object in place using a consistent path notation.
public static JsonObject ReplaceValues(this JsonObject root, Dictionary<string, string?> replacements, string separator = ":")
Parameters
rootJsonObjectThe root JSON object to modify.
replacementsDictionary<string, string>A mapping from path to new value. Paths can be:
-
Simple property names (e.g.,
"name"); only updated if the property exists on the root. -
separator-joined nested paths (e.g.,"user:address:city"). -
Paths that step into arrays using bare numeric segments only (e.g.,
"users:0:name").
Note: bracketed indices like
[0]are not interpreted as array indices by this method; they are treated as ordinary property names. Entries with empty keys are skipped.-
Simple property names (e.g.,
separatorstringThe string used to split path segments. Defaults to
":".
Returns
- JsonObject
The same System.Text.Json.Nodes.JsonObject instance passed in, after modifications.
Examples
var obj = JsonNode.Parse("""{ "name": "John", "user": { "address": { "city":"NYC" } }, "users": [ { "name":"A" }, { "name":"B" } ] }""").AsObject();
obj.ReplaceValues(new Dictionary<string,string?>
{
["name"] = "Jane", // replaces root-level property if present
["user:address:city"] = "LA", // traverses nested objects if present
["users:1:name"] = "Beta", // updates within existing arrays (bare numeric index)
});
// {"name":"Jane","user":{"address":{"city":"LA"}},"users":[{"name":"A"},{"name":"Beta"}]}
Remarks
Behavior for each entry:
-
Simple keys (no
separator): the root property is updated only if it already exists. - Nested paths: the structure is traversed segment by segment. The final value is set only if the full path exists (the last object property must already exist; array indices must be in bounds).
-
Fallbacks when traversal cannot complete: if an intermediate object is missing a segment, the
remaining
separator-joined path is set as a literal property on the deepest object reached (added if absent); otherwise, if the root itself contains a literal property whose name equals the full key, that property is updated. - No intermediate containers (objects or arrays) are ever created.
- Values are stored using System.Text.Json.Nodes.JsonValue.Create(string, System.Text.Json.Nodes.JsonNodeOptions?), so all replacement values become JSON strings (or JSON null); existing value kinds are not preserved at the target path.
This method modifies root in place and returns it for chaining.
Exceptions
- ArgumentNullException
Thrown when
replacementsis null.
Unflatten(IDictionary<string, string?>, string)
Reconstructs a hierarchical JSON object from flattened key/value pairs, reversing
Flatten(JsonNode, string).
Keys are parsed as separator-joined paths in which bracketed segments ([0], [1], ...)
denote array indices.
public static JsonObject Unflatten(IDictionary<string, string?> flattened, string separator = ":")
Parameters
flattenedIDictionary<string, string>A dictionary of flattened key/value pairs where:
-
Key: a path using
separatorbetween object property segments and[index]notation for array elements (e.g.,"user:tags:[0]"). -
Value: the string representation of the value (
nullfor JSON null values).
-
Key: a path using
separatorstringThe string used to split keys into path segments. Must match the separator used when flattening. Defaults to
":".
Returns
- JsonObject
A new System.Text.Json.Nodes.JsonObject representing the reconstructed hierarchical structure. Nested objects and arrays are created as needed based on the path notation; all leaf values are stored as JSON strings (or JSON null).
Examples
var flattened = new Dictionary<string, string?>
{
["user:name"] = "John",
["user:addresses:[0]:city"] = "New York",
["user:addresses:[0]:zip"] = "10001",
["user:tags:[0]"] = "admin",
["user:tags:[1]"] = "user",
};
var json = JsonExtensions.Unflatten(flattened);
// {"user":{"name":"John","addresses":[{"city":"New York","zip":"10001"}],"tags":["admin","user"]}}
Remarks
The method handles common scenarios including:
- Creating nested objects and arrays as needed.
- Managing transitions between object and array contexts.
- Overwriting existing values when the same path is encountered multiple times.
Note: array indices are applied in append order; sparse or non-sequential indices are not padded with nulls.
Path parsing rules:
separatorseparates object property names.-
Square brackets with numbers (
[0],[1], etc.) indicate array indices. -
Mixed object/array paths are supported (e.g.,
"users:[0]:name").
Because flattened values are strings, all reconstructed leaf values are JSON strings; the original JSON value kinds (number, boolean) are not restored.
Exceptions
- ArgumentNullException
Thrown when
flattenedis null.