JSON

edit

Meziantou.Framework.Language.Json parses JSON into a tree that keeps every character, so you can change a value of a package.json or an appsettings.json and keep its comments, its indentation and its key order.

dotnet package add Meziantou.Framework.Language.Json

Parse a document

JsonSyntaxTree.ParseText reads a string. Comments and trailing commas are accepted without a diagnostic:

var tree = JsonSyntaxTree.ParseText("""
{
  // the name of the thing
  "name": "value",
  "items": [1, 2,],
}
""");

// Comments and trailing commas are accepted, so there is nothing to report here.
Assert.Empty(tree.GetDiagnostics());

// And the text comes back exactly as it went in.
Assert.Equal(tree.GetText().Text, tree.GetRoot().ToFullString());

The shape of a tree

A JsonDocumentSyntax holds the root value in Value. A document with several values, which is an error, keeps them all in Values.

NodeIsHolds
JsonObjectSyntax{ … }Members, a separated list of JsonMemberSyntax, and GetMember(name)
JsonMemberSyntax"name": valueNameToken, Name, ColonToken, Value
JsonArraySyntax[ … ]Elements, a separated list of values
JsonStringSyntax"text"Value, the string without its quotes and escapes
JsonNumberSyntax42, -1.5e3Text, the number as written
JsonLiteralSyntaxtrue, false, nullLiteralToken; its kind tells which literal it is
JsonSkippedTextSyntaxText the parser could not useThe skipped tokens

The commas belong to the lists, not to the values: see Lists. A member name is a token of its member, not a JsonStringSyntax.

Whitespace and comments up to the end of a line belong to the token that ends the line, and the rest to the next token, so a comment on its own line goes with the member below it. See Trivia.

Query with JSONPath

The package includes extension methods that run a JSONPath query on a tree. EvaluateValue returns the node of a single result, and Evaluate every result:

var tree = JsonSyntaxTree.ParseText("""{ "items": [1, 2, 3] }""");

var value = JsonPath.Parse("$.items[1]").EvaluateValue(tree);
Assert.Equal("2", value?.ToString());

JsonPath is in the Meziantou.Framework.Json namespace. The results are nodes of the tree, so you can replace them.

EvaluateValue uses the value-only first-match path; Strict mode still validates later branches. The tree and node extension overloads also accept JsonPathEvaluationOptions for cooperative cancellation and resource limits. See Cancel or bound evaluation.

Edit a document

Replace a value with a node built by SyntaxFactory. The rest of the document is kept as it is:

var tree = JsonSyntaxTree.ParseText("""{ "version": "1.0.0", "other": 1 }""");
var version = ((JsonObjectSyntax)tree.GetRoot().Value!).GetMember("version")!;

JsonDocumentSyntax updated = tree.GetRoot().ReplaceNode(version.Value, SyntaxFactory.JsonString("2.0.0"));

Assert.Equal("""{ "version": "2.0.0", "other": 1 }""", updated.ToFullString());

Diagnostics

var messages = new List<string>();
foreach (var diagnostic in JsonSyntaxTree.ParseText("{\"a\": }").GetDiagnostics())
{
    var line = diagnostic.Location.GetLineSpan().Start;
    messages.Add($"{diagnostic.Id} ({line.Line},{line.Character}): {diagnostic.Message}");
}

Assert.Equal(["JSON0007 (0,6): Expected a JSON value."], messages);
IdReported for
JSON0001An unterminated block comment
JSON0002An unterminated string
JSON0003A bad escape sequence
JSON0004A malformed number
JSON0005An unexpected token or comma
JSON0006A missing {, }, [, ], or :
JSON0007A missing value
JSON0008A missing or unquoted property name
JSON0009A missing comma
JSON0010Data after the root value
JSON0011An unescaped control character inside a string
JSON0012Objects and arrays nested more than 256 deep
JSON0013Whitespace JSON does not allow, such as a form feed or a non-breaking space
JSON0014A string in single quotes
JSON0015A property name used twice in one object (a warning)

Every diagnostic is an error except JSON0015: the JSON specification only says that names should be unique.

What is accepted

The grammar is RFC 8259, with two extensions accepted without a diagnostic: // and /* */ comments wherever whitespace may appear, and a trailing comma after the last member or element. A byte order mark at the start of the text is ignored. Everything else the RFC does not allow is reported, including an empty document, a raw tab in a string, and NaN.

Mistakes

A common mistake is reported once and read as what it was meant to be, so the rest of the document is read as usual:

  • A string without its closing quote ends at the end of its line.
  • {name: 1} and {'name': 'value'} give a member called name, with a diagnostic on the quotes.
  • 0x1F, 1.2.3, -Infinity and +1 are each one malformed number.
  • A closing brace or bracket ends every construct it closes: in {"a": [1, 2}, the array is reported as missing its ], and the object ends at the }.
  • A stray } after the root value is reported on its own, and the root value is kept.

Walk a tree

JsonSyntaxWalker, JsonSyntaxVisitor and JsonSyntaxRewriter have a method per node type, such as VisitJsonObject and VisitJsonString. See Visitors and rewriters, whose examples use JSON.