Table of Contents

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
object
JsonExtensions

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

node JsonNode

The JSON node to flatten. Can be a System.Text.Json.Nodes.JsonObject, System.Text.Json.Nodes.JsonArray, or primitive value.

separator string

The 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 separator between object property segments and [index] notation for array elements (e.g., "user:tags:[0]").
  • Values are the string representation of the JSON values (null for JSON null values), so the original JSON value kinds are not preserved.

The source node is not modified.

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 node is 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

root JsonObject

The JSON object to modify.

path string

The 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 no separator, it is treated as a simple, root-level property name. An empty path is ignored and the object is returned unchanged.

value string

The new value to assign at the specified path. Use null for JSON null.

separator string

The 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.

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 path is 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

root JsonObject

The root JSON object to modify.

replacements Dictionary<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.

separator string

The 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 replacements is 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

flattened IDictionary<string, string>

A dictionary of flattened key/value pairs where:

  • Key: a path using separator between object property segments and [index] notation for array elements (e.g., "user:tags:[0]").
  • Value: the string representation of the value (null for JSON null values).
separator string

The 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:

  • separator separates 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 flattened is null.