Table of Contents

Getting Started

This page takes you from installing the package to flattening, rebuilding, and updating your first System.Text.Json node tree. Read Introduction first if you are still deciding which API surface fits your problem.

Installation

dotnet add package Invex.Extensions.Json

Supported target frameworks: net10.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 Introduction for when to use each and 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