JSON.cs Quick Guide
JSON is a value-oriented C# type representing an ordinary JSON value:
struct JSON { Object, Array, String, Int, Number, Bool, Null, Proxy }
Use compact literals to construct values:
JSON person = JSON.NewObject( ("name", "Øystein"), ("age", 36), ("admin", true), ("tags", JSON.NewArray( "math", "programming" )), ("middleName", JSON.Null)); JSON number = 3.14;
Reading values
Typed accessors return optionals and fail with null when the type is wrong:
value.String // string? value.Int // long? value.Number // double? value.Bool // bool? value.Object // detached Dictionary<string, JSON>? value.Array // detached List<JSON>? value.IsNull // bool
The .Int accessor converts a decimal value only when it has no fractional part.
Object keys and array indexes return JSON?:
string? name = person["name"]?.String; long? age = person["age"]?.Int; string? firstTag = person["tags"]?[0]?.String;
Mutating values
JSON has value semantics. Assigning it creates an independent value logically, while arrays and dictionaries benefit from copy-on-write storage.
JSON first = JSON.NewObject( ("name", "Abel"), ("age", 46) ); JSON second = first; second["name"] = "Zippel"; second["age"] = 37; second["active"] = true;
Assigning null removes an object key or array element:
value["temporary"] = null;
Use JSON.Null to retain a key or element as JSON null:
value["middleName"] = JSON.Null;
Array mutation uses zero-based indexes:
var numbers = JSON.NewArray([ 10, 20, 30 ]); numbers[1] = 25; numbers[0] = null; // removes the first element
Object members can also be removed explicitly:
value.RemoveValue("name");
Object and Array return detached snapshots. Mutating one does not mutate the original JSON:
JSON state = JSON.NewObject(); state["count"] = 1; var snapshot = state; state["count"] = 2; // snapshot["count"] is still 1
Paths
Paths provide one operation for walking through nested objects, arrays, and
proxies. A path component is normally a string for an object key or an int
for an array index. The ordinary one-component indexers remain the shortest
form:
state["user"] state[0]
The variadic indexer accepts mixed components directly:
var name = state["users", 1, "name"]?.String; state["users", 1, "name"] = new JSON("Ada");
The explicit methods accept an object[] path. Useful when the path is built
dynamically.
object[] path = ["users", 1, "name"]; var name = state.GetPath(path)?.String; bool changed = state.SetPath(path, new JSON("Ada"));
Serialization
Stringify produces valid textual JSON:
string text = JSON.Stringify(value); string compact = JSON.Stringify(value, prettyPrinted: false); JSON restored = JSON.Parse(text);
ToString() is a friendly JavaScript-like display operation, not strict JSON
serialization:
new JSON("Hello World").ToString(); // Hello World JSON.Stringify(new JSON("Hello World")); // "Hello World"
Object keys are sorted during serialization. NaN and infinity are rejected
because they are not valid JSON numbers.
IJSONProxy interface
IJSONProxy provides an in-memory value that resolves whenever it is read or serialized. It is represented in textual JSON exactly the same as its resolved JSON and is useful for live or externally owned values:
sealed class Counter : IJSONProxy { private int value; public JSON ResolveJSONProxy() { value += 1; return new JSON(value); } } var counter = JSON.NewProxy(new Counter()); counter.Proxy is Counter; // true counter.Int; // 1 counter.Int; // 2
An object-shaped proxy can also receive writes to selected keys. It is not required to make the proxy resolve to anything for this.
sealed class Settings : IJSONProxy { public string Title { get; set; } = "Initial"; public int Skip { get; set; } = 1; public JSON ResolveJSONProxy() => JSON.NewObject( ("title", new JSON(Title)), ("skip", new JSON(Skip)) ); public void SetJSONProxyValue(JSON key, JSON? value) { switch (key.String) { case "title": if (value is { } title) Title = title.String ?? title.ToString(); break; case "skip": if (value is { } skip) Skip = (int)(skip.Int ?? Skip); break; } } } var settings = JSON.NewProxy(new Settings()); settings["title"] = new JSON("Updated"); settings["skip"] = new JSON(10);
Reads resolve proxies as they traverse. Changing a value inside a detached resolved proxy does not write back unless the proxy provides the appropriate setter behavior.
Proxy resolution is not cached. Proxies may occur inside ordinary objects and arrays, and may resolve to other proxies. Resolution does not perform cycle detection.
Equality compares ordinary JSON structurally and deeply. A proxy is resolved before comparison, so equality involving a stateful proxy may have side effects or produce different answers at different times.
.NET object conversion
JSON.FromObject accepts JSON-compatible .NET primitives, dictionaries, and
enumerables:
var value = JSON.FromObject(new Dictionary<string, object?> { ["name"] = "Ada", ["age"] = 36 });
Unsupported values throw JSON.Error.
Running the examples
The executable examples and assertions are in Program.cs. With the .NET SDK
on the path:
dotnet run
Licensing for use
© 2026 Hypervariety Custom Programming, LLC. All rights reserved. All commercial and non-commercial use is permitted by the author, as long as this copyright message accompanies the product and source.