Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types - #125139

Merged
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter
Mar 18, 2026
Merged

Fix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive types#125139
eiriktsarpalis merged 5 commits into
mainfrom
copilot/fix-breaking-change-jsonnodeconverter

Conversation

CopilotAI commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Description

JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber (introduced in .NET 10) throw InvalidOperationException on GetValue<object>() because their TryGetValue<T> methods don't handle typeof(T) == typeof(object). The older JsonValueOfElement handled this implicitly via if (Value is TypeToConvert element).

Additionally, TryGetValue<JsonElement?>() was broken on all three types because they use explicit typeof(T) == typeof(JsonElement) checks which don't match typeof(JsonElement?). The sibling JsonValueOfElement handles this naturally via Value is TypeToConvert pattern matching, but the new types needed explicit typeof(T) == typeof(JsonElement?) checks.

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=converter.Read(refreader,typeof(JsonNode),options);// .NET 10: throws InvalidOperationException// .NET 9: works finevaro=node.GetValue<object>();

Changes

  • All three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber): Added typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) to the JsonElement branch of TryGetValue<T>. This matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) naturally handles JsonElement, JsonElement?, and object by boxing the JsonElement value. GetValue<object>() now returns JsonElement uniformly across all types for backward compatibility.
  • Added TryGetValue<object> assertions to existing TryGetValue_From* tests and a new GetValue_Object theory exercising the converter code path — asserts JsonElement return type for all primitive kinds
  • Added TryGetValue_NullableTypes_Deserialized test covering JsonElement? on all three types, bool? on JsonValueOfJsonBool, and all numeric nullable types on JsonValueOfJsonNumber
Original prompt

This section details on the original issue you should resolve

<issue_title>Breaking change in JsonNodeConverter</issue_title>
<issue_description>### Description

Using a JsonConverter to read a string node, .NET 9 and .NET 10 show different behavior.

The new System.Text.Json.Nodes.JsonValueOfJsonString introduced in .NET 10 cannot be converted to an object using GetValue<object>(), resulting in an exception.

Reproduction Steps

See Repo at https://github.com/NiceWaffel/jsonnodeconverter-repro

varreader=newUtf8JsonReader("\"Hello World!\""u8);reader.Read();varnode=(SerializationContext.Default.JsonNode.ConverterasJsonConverter<JsonNode>)?.Read(refreader,typeof(JsonNode),SerializationContext.Default.Options);Debug.Assert(node!=null,"We should get a valid JsonNode.");// Different type in .NET 8, .NET 9 and .NET 10// .NET 8: System.Text.Json.Nodes.JsonValuePrimitive`1[System.Text.Json.JsonElement]// .NET 9: System.Text.Json.Nodes.JsonValueOfElement// .NET 10: System.Text.Json.Nodes.JsonValueOfJsonStringConsole.WriteLine(node.GetType());// Exception in .NET 10 (no exception in .NET 8)// System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.varo=node.GetValue<object>();Console.WriteLine(o);

Expected behavior

The behavior in .NET 8, .NET 9 and .Net 10 should be the same. No exception should be thrown.

Actual behavior

In .NET 10, a string JsonNode (read as JsonValueOfJsonString) cannot be converted to an object, resulting in an exception.

Unhandled exception. System.InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Object'.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType)
at System.Text.Json.Nodes.JsonValueOfJsonString.GetValue[T]()

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

JsonValueOfJsonString was introduced with #116798</issue_description>

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


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Handle typeof(T) == typeof(object) in TryGetValue for
JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
This fixes InvalidOperationException when calling GetValue<object>()
on these types that were introduced in .NET 10.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot@PranavSenthilnathan Based on the changes made in #116798 are there any further variants we should be considering?

The three types fixed here — JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber — are the only ones affected. They're the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks.

The other subclasses are not affected:

  • JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> don't override TryGetValue; they inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — this naturally handles object via boxing.
  • JsonValueOfElement also uses if (Value is TypeToConvert element) as its first check, so JsonElement boxes to object successfully.

No further variants to consider.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in System.Text.Json.Nodes where JsonValueOfJsonString/Bool/Number (used by JsonValueConverter/JsonNodeConverter for primitives) throw on GetValue<object>() by adding explicit typeof(T) == typeof(object) handling in their TryGetValue<T> implementations.

Changes:

  • Add object handling in TryGetValue<T> for JsonValueOfJsonString, JsonValueOfJsonBool, and JsonValueOfJsonNumber.
  • Extend existing TryGetValue_From* tests with TryGetValue<object> assertions.
  • Add a new GetValue_Object theory validating GetValue<object>() for deserialized primitive JsonValues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.csAdds explicit object support in TryGetValue<T> for optimized primitive-backed JsonValue implementations.
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.csAdds regression tests covering TryGetValue<object> and GetValue<object> for primitives.
Comments suppressed due to low confidence (2)

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:82

  • TryGetValue() for string values now returns a decoded string rather than a boxed JsonElement. In .NET 9 (JsonValueOfElement) and in JsonNode.Parse(...) today, GetValue() yields a JsonElement, so this changes the observable runtime type and may be a compatibility break. Please confirm the intended contract for GetValue/TryGetValue() on deserialized primitives; if compatibility is desired, consider returning JsonElement for object here (and similarly for bool) or update the public docs accordingly.
     if (typeof(T) == typeof(string) || typeof(T) == typeof(object))
    {
    string? result = JsonReaderHelper.TranscodeHelper(_value.Span);
    Debug.Assert(result != null);
    value = (T)(object)result;
    return true;
    }
    

    src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfJsonPrimitive.cs:163

    • TryGetValue() for booleans returns boxed bool, which differs from the historical JsonValueOfElement behavior (boxed JsonElement) and from JsonNode.Parse(...) today. If the goal is to preserve .NET 9 behavior for GetValue(), consider returning JsonElement for object here as well (or document that object returns CLR primitives for these optimized deserialized nodes).
       if (typeof(T) == typeof(bool) || typeof(T) == typeof(bool?) || typeof(T) == typeof(object))
      {
      value = (T)(object)_value;
      return true;
      }
      

      You can also share your feedback on Copilot code review. Take the survey.

@stephentoub

Copy link
Copy Markdown
Member

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

CopilotAI commented Mar 4, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476

Holistic Assessment

Motivation: The regression is real and well-documented — GetValue<object>() throws InvalidOperationException on the three JsonValueOfJsonPrimitive types introduced in .NET 10 (PR #116798). The documented contract for TryGetValue (see JsonValue.cs line 28) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) checks to each TryGetValue<T> override is the correct minimal fix. The only question is what value to return for the object case, which has implications for backward compatibility.

Summary: ⚠️ Needs Human Review. The fix is correct for the stated regression (no more exception), but there is a behavioral inconsistency worth a maintainer decision: GetValue<object>() now returns different types depending on how the node was created (Parse path vs Deserialize path). Details below.


Detailed Findings

⚠️ Behavioral inconsistency — GetValue<object>() returns different types depending on creation path

In .NET 9, both JsonNode.Parse("\"hello\"") and JsonSerializer.Deserialize<JsonValue>("\"hello\"") produced JsonValueOfElement, so GetValue<object>() returned a boxed JsonElement in all cases.

With this fix:

  • JsonNode.Parse("\"hello\"").GetValue<object>() → boxed JsonElement (via JsonValueOfElement, unchanged)
  • JsonSerializer.Deserialize<JsonValue>("\"hello\"").GetValue<object>()string (via JsonValueOfJsonString, new behavior)

Similarly for booleans (bool vs JsonElement). Numbers are consistent (both return JsonElement).

This asymmetry is a deliberate choice by the PR (stated in the description), and arguably more useful. But it means code migrating from .NET 9 that does node.GetValue<object>() is JsonElement would get true with Parse-created nodes but false with Deserialize-created nodes.

Alternative: For maximum backward compatibility, all three types could return JsonElement for typeof(T) == typeof(object) by placing the object check in the JsonElement branch. This would match JsonValueOfElement's behavior. The trade-off is returning a less natural type.

This is a design decision for the maintainers.

⚠️ Stale documentation on TryGetValue

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale — deserialized primitives are backed by JsonValueOfJsonString/Bool/Number, not JsonElement. With this PR's fix, GetValue<object>() returns string/bool (not JsonElement) for string/bool values created via deserialization. The doc should be updated or marked as a follow-up.

💡 Test coverage — TryGetValue_From* assertions test JsonValueOfElement, not the fix

The three new Assert.True(jValue.TryGetValue(out object _)) lines added to TryGetValue_FromString, TryGetValue_FromNumber, and TryGetValue_FromBoolean use JsonNode.Parse(...), which creates JsonValueOfElement nodes. They do not exercise the JsonValueOfJsonPrimitive types this PR fixes. These assertions test pre-existing working behavior.

Only the GetValue_Object theory (which uses JsonSerializer.Deserialize<JsonValue>) actually tests the fix. The assertions aren't wrong — they're useful documentation of expected behavior — but they don't provide coverage for the regression.

Consider adding a parallel TryGetValue_From*_Deserialized variant or extending GetValue_Object to also assert TryGetValue<object> to make the coverage more explicit.

✅ Correctness — fix is sound for the three affected types

The three types (JsonValueOfJsonString, JsonValueOfJsonBool, JsonValueOfJsonNumber) are the only JsonValue subclasses that directly extend JsonValue (not JsonValue<TValue>) and override TryGetValue<T> with manual type checks. The other subclasses (JsonValuePrimitive<T>, JsonValueCustomized<T>, JsonValueOfElement) handle object correctly through Value is T pattern matching or the JsonValue<TValue> base class. No further variants are needed.

✅ No public API surface changes

No changes to ref/ assembly files. The fix is purely internal behavioral correction.

… and add test coverage
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
CopilotAI changed the title Fix GetValue<object>() on JsonValueOfJsonPrimitive typesFix GetValue<object>() and TryGetValue<JsonElement?> on JsonValueOfJsonPrimitive typesMar 4, 2026
Per reviewer feedback from @PranavSenthilnathan: GetValue<object>() should
return JsonElement (not string/bool) for all three JsonValueOfJsonPrimitive
types, matching the behavior of JsonValueOfElement where `Value is TypeToConvert`
returned JsonElement for object. Updated tests accordingly.
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

CopilotAI commented Mar 5, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post the analysis/comments as a comment on this PR

🤖 Copilot Code Review — PR #125476 (updated after latest changes)

Holistic Assessment

Motivation: Real regression — GetValue<object>() throws InvalidOperationException on JsonValueOfJsonString/Bool/Number types introduced in .NET 10 (PR #116798). The TryGetValue contract (line 28 of JsonValue.cs) explicitly states "Specifying the object type for {T} will always succeed."

Approach: Adding typeof(T) == typeof(object) and typeof(T) == typeof(JsonElement?) checks to the JsonElement branch in each TryGetValue<T> override is the correct minimal fix. Returning JsonElement for object matches JsonValueOfElement's behavior where Value is TypeToConvert (line 54) boxes JsonElement to object.

Summary: ✅ LGTM. The fix is correct, backward-compatible, and well-tested. All three affected types now handle object, JsonElement, and JsonElement? identically to how JsonValueOfElement handles them. No other JsonValue subclasses are affected.


Detailed Findings

✅ Correctness — fix matches JsonValueOfElement behavior exactly

The three JsonElement branches now include typeof(T) == typeof(JsonElement?) and typeof(T) == typeof(object), which mirrors exactly what JsonValueOfElement.TryGetValue does at line 54 via if (Value is TypeToConvert element) — that pattern matches JsonElement, JsonElement?, and object (since JsonElement is a struct that boxes to object).

✅ No other variants affected

Verified: JsonValuePrimitive<TValue> and JsonValueCustomized<TValue> inherit JsonValue<TValue>.TryGetValue which uses if (Value is T returnValue) — handles object naturally. JsonValueOfElement uses the same pattern. Only these three types needed fixing.

✅ Test coverage

  • GetValue_Object theory covers GetValue<object>() returning JsonElement for all four primitive JSON types (string, number, true, false) via the deserialization path.
  • TryGetValue_NullableTypes_Deserialized covers JsonElement? and relevant nullable value types on deserialized primitives.
  • Existing TryGetValue_From* tests also assert TryGetValue<object> on JsonValueOfElement (Parse path).

💡 Stale doc comment on TryGetValue (follow-up)

JsonValue.cs line 29 says: "The underlying value of a JsonValue after deserialization is an instance of JsonElement." This was accurate before PR #116798 but is now stale since deserialized primitives are JsonValueOfJsonString/Bool/Number. With GetValue<object>() returning JsonElement for all types, the behavioral contract still holds, but the implementation detail described is incorrect. Could be updated as a follow-up.

✅ No public API surface changes

No changes to ref/ assembly files. Purely internal behavioral correction.

@eiriktsarpalis

Copy link
Copy Markdown
Member

/ba-g test failures unrelated.

@eiriktsarpalis
eiriktsarpalis merged commit 1ab6d1d into mainMar 18, 2026
87 of 90 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the copilot/fix-breaking-change-jsonnodeconverter branch March 18, 2026 16:08
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Apr 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breaking change in JsonNodeConverter

6 participants

@stephentoub@eiriktsarpalis@PranavSenthilnathan@jkotas