Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 3, 2026

Description

JsonNode.GetPath() produces invalid JSON Path syntax when property names contain characters requiring escaping. Two issues:

  1. Property names starting with $ (e.g., $defs, $ref) used dot notation ($.$defs) instead of bracket notation ($['$defs'])
  2. Single quotes and backslashes in property names were not escaped in bracket notation

Before:

JsonNode.Parse("""{"$defs":{"foo['bar":"baz"}}""")["$defs"]["foo['bar"].GetPath();
// Returns: $.$defs['foo['bar']  (invalid JSON Path)

After:

// Returns: $['$defs']['foo[\'bar']  (valid JSON Path)

Changes:

  • Add $ to special characters triggering bracket notation in JsonReaderHelper.cs
  • Add AppendEscapedPropertyName helper methods that escape '\' and \\\, optimized using IndexOfAny to append prefix slices directly instead of per-character iteration
  • Update JsonObject.GetPath, WriteStack.AppendPropertyName, and ReadStack.AppendPropertyName to use escaping
  • Update test expectations to reflect corrected JSON Path syntax

Customer Impact

Users receiving invalid JSON Path strings from GetPath() that cannot be parsed or used with JSON Path tools.

Regression

No. This behavior has existed since the feature was introduced.

Testing

  • Added new test cases covering $-prefixed names, single quotes, backslashes, and combinations
  • Added reproduction case from issue
  • All 49,808 System.Text.Json tests pass

Risk

Low. The change makes output spec-compliant. Existing consumers expecting the old (invalid) format may need adjustment.

Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.

Original prompt

This section details on the original issue you should resolve

<issue_title>JsonNode.GetPath() doesn't consider character escaping</issue_title>
<issue_description>### Description

The JsonNode.GetPath() generally does well for C#-valid property names, but any string is valid as a key in JSON, and when a key has characters that need escaping, nothing happens and you get an invalid (unparsable) JSON Path as a result.

Reproduction Steps

JsonNode.Parse("""{"$defs":{"foo['bar":"baz"}}""")["$defs"]["foo['bar"].GetPath();

Expected behavior

Returns $.$defs['foo[\'bar'].

Returns $['$defs']['foo[\'bar'].

  • The single quote needs to be escaped.
  • $defs is not valid as a shorthand property name, so it needs to be in bracketed syntax.

Actual behavior

Returns $.$defs['foo['bar'].

Regression?

Unknown

Known Workarounds

None.

Configuration

Seems to happen everywhere. dotnetfiddle, Linux.

Will update with more details in a comment.

Other information

Originally reported json-everything/json-everything#406.
</issue_description>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

Description

The JsonNode.GetPath() generally does well for C#-valid property names, but any string is valid as a key in JSON, and when a key has characters that need escaping, nothing happens and you get an invalid (unparsable) JSON Path as a result.

Reproduction Steps

JsonNode.Parse("""{"$defs":{"foo['bar":"baz"}}""")["$defs"]["foo['bar"].GetPath();

Expected behavior

Returns $.$defs['foo[\'bar'].

Actual behavior

Returns $.$defs['foo['bar'].

Regression?

Unknown

Known Workarounds

None.

Configuration

Seems to happen everywhere. dotnetfiddle, Linux.

Will update with more details in a comment.

Other information

Originally reported json-everything/json-everything#406.

Author: gregsdennis
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 3, 2026 22:22
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix JsonNode.GetPath method for character escaping Fix JsonNode.GetPath() to properly escape characters in property names Jan 3, 2026
Copilot AI requested a review from stephentoub January 3, 2026 22:36
@stephentoub stephentoub marked this pull request as ready for review January 4, 2026 00:32
Copilot AI review requested due to automatic review settings January 4, 2026 00:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes JsonNode.GetPath() to produce valid JSON Path syntax when property names contain special characters requiring escaping. The fix addresses two specific issues: (1) property names starting with $ now use bracket notation instead of invalid dot notation, and (2) single quotes and backslashes in property names are now properly escaped within bracket notation.

Key Changes:

  • Added $ to the set of special characters that trigger bracket notation in JSON Path
  • Implemented AppendEscapedPropertyName helper methods to escape ' as \' and \ as \\ within bracket notation
  • Updated all JSON Path generation sites (JsonObject.GetPath, WriteStack.AppendPropertyName, ReadStack.AppendPropertyName) to use the new escaping

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
JsonReaderHelper.cs Added $ to special characters set; implemented two overloads of AppendEscapedPropertyName for escaping quotes and backslashes
JsonObject.cs Updated GetPath to call AppendEscapedPropertyName when using bracket notation
WriteStack.cs Updated AppendPropertyName to call AppendEscapedPropertyName when using bracket notation
ReadStack.cs Updated AppendPropertyName to call AppendEscapedPropertyName when using bracket notation
ParentPathRootTests.cs Added comprehensive test cases covering $-prefixed names, quotes, backslashes, and nested scenarios
PolymorphicTests.CustomTypeHierarchies.cs Updated test expectations from dot notation (e.g., $.$type) to bracket notation (e.g., $['$type'])
ReferenceHandlerTests.Deserialize.cs Updated test expectations from dot notation to bracket notation for $-prefixed metadata properties
ConstructorTests.Exceptions.cs Updated test expectations from dot notation to bracket notation for $-prefixed properties

return;
}

// Slow path: escape single quotes and backslashes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, instead of using ContainsAny, this can use IndexOfAny. If the result is < 0, then it can just append the propertyName and return. If it's > 0, it can append propertyName.Slice(0, i). And then it can walk character by character starting from i as is done below. That way, if the character that needs escaping is later rather than earlier, we don't have to spend time individually appending each individual character up until that point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cd3590f. Used IndexOfAny to find the first character needing escaping, append the prefix slice directly (if any), then walk character by character only from that position.

/// Appends a property name escaped for use in JSON Path single-quoted bracket notation.
/// Escapes single quotes as \' and backslashes as \\.
/// </summary>
public static void AppendEscapedPropertyName(this StringBuilder builder, string propertyName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, same comment as above. Under #if NET, StringBuilder supports appending a span, so it can append a slice. Otherwise, StringBuilder supports appending a pointer/length, so it can pin the slice and append it that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cd3590f. Applied the same optimization. Under #if NET, StringBuilder appends the span slice directly. Otherwise, it pins the string and uses the pointer/length overload.

…acter iteration for prefix

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JsonNode.GetPath() doesn't consider character escaping

2 participants