Skip to content

Reduce SumConverter exceptions by peeking at array element types#83708

Merged
ToddGrun merged 11 commits into
dotnet:mainfrom
ToddGrun:dev/toddgrun/reduce-sumconverter-deserialization-exceptions
May 18, 2026
Merged

Reduce SumConverter exceptions by peeking at array element types#83708
ToddGrun merged 11 commits into
dotnet:mainfrom
ToddGrun:dev/toddgrun/reduce-sumconverter-deserialization-exceptions

Conversation

@ToddGrun
Copy link
Copy Markdown
Contributor

@ToddGrun ToddGrun commented May 14, 2026

SumConverter previously used exception-based type probing for all SumType arms, attempting deserialization and catching failures. For array variants like SumType<string[], VSInternalCommitCharacter[]> (used by VSInternalCompletionItem.VsCommitCharacters and VSInternalCompletionList.CommitCharacters), this caused InvalidOperationException on every mismatched attempt.

This change adds array element peeking: before attempting deserialization, it reads the first array element's token type to determine compatibility. A string token matches string[] but not VSInternalCommitCharacter[], and a StartObject token matches VSInternalCommitCharacter[] but not string[], eliminating exception-based fallback for these cases.

Refactors IsTokenCompatibleWithType into a core Type overload so array element peeking can reuse the same compatibility logic. Adds SumConverterTypeFilteringTests that verify correct arm selection and assert zero first-chance exceptions during deserialization.

Microsoft Reviewers: Open in CodeFlow

SumConverter previously used exception-based type probing for all SumType arms, attempting deserialization and catching failures. For array variants like SumType<string[], VSInternalCommitCharacter[]> (used by VSInternalCompletionItem.VsCommitCharacters and VSInternalCompletionList.CommitCharacters), this caused InvalidOperationException on every mismatched attempt.

This change adds array element peeking: before attempting deserialization, it reads the first array element's token type to determine compatibility. A string token matches string[] but not VSInternalCommitCharacter[], and a StartObject token matches VSInternalCommitCharacter[] but not string[], eliminating exception-based fallback for these cases.

Refactors IsTokenCompatibleWithType into a core Type overload so array element peeking can reuse the same compatibility logic. Adds SumConverterTypeFilteringTests that verify correct arm selection and assert zero first-chance exceptions during deserialization.
@ToddGrun ToddGrun requested a review from kayle May 14, 2026 21:25
@ToddGrun ToddGrun requested a review from a team as a code owner May 14, 2026 21:25
Copilot AI review requested due to automatic review settings May 14, 2026 21:25
@ToddGrun ToddGrun requested a review from a team as a code owner May 14, 2026 21:25
@ToddGrun
Copy link
Copy Markdown
Contributor Author

@kayle -- If this is taken on the roslyn side, I'd be interesting in doing something similar on the VS side.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the LSP SumConverter to reduce exception-driven “probe deserialization” when a SumType contains multiple array arms by peeking the first array element’s token type to pre-filter incompatible arms. It also adds unit tests intended to validate correct arm selection and to assert that deserialization no longer relies on internal exceptions for these cases.

Changes:

  • Refactors IsTokenCompatibleWithType to route through a Type-based overload and adds array element peeking for JsonTokenType.StartArray.
  • Introduces IsSerializedAsJsonPrimitiveType to support StartObject/array-element compatibility decisions.
  • Adds SumConverterTypeFilteringTests covering several array disambiguation scenarios and asserting no first-chance exceptions during deserialization.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs Adds array-element peeking and refactors token/type compatibility logic to avoid exception-based fallback.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests/Serialization/SumConverterTypeFilteringTests.cs Adds tests for array-arm selection and attempts to validate “no internal exceptions” during deserialization.

Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs
Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs Outdated
Array element peeking incorrectly rejected arrays whose element type has a custom JsonConverter (e.g., SumType<string, int>[]). The peeking logic would evaluate the element's token type against IsTokenCompatibleWithType, which doesn't understand converter-driven types that accept multiple token kinds. This caused a "No sum type match" JsonException at runtime. Fix: skip peeking when the element type has a JsonConverterAttribute, falling back to the existing try/catch path.

Also adds a [Collection] attribute to SumConverterTypeFilteringTests to prevent xUnit parallel execution from causing false positives in the FirstChanceException assertions, and fixes em dashes in comments.
Copy link
Copy Markdown
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

Surprised the tests are in a Razor folder, but it's always nice to be invited to something :)

LGTM though

Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs Outdated
Move the JsonConverterAttribute check from IsArrayElementCompatible into IsTokenCompatibleWithType, where it protects all callers rather than just the array peeking path. Add a regression test for SumType element arrays (e.g., SumType<string, int>[]) that would fail without this guard.

Move SumConverterTypeFilteringTests from the Razor test project to ProtocolUnitTests/Converters, colocating them with the product code they test. Fix license header and file encoding to match repo conventions.
Copilot AI review requested due to automatic review settings May 15, 2026 01:57
@ToddGrun
Copy link
Copy Markdown
Contributor Author

Surprised the tests are in a Razor folder, but it's always nice to be invited to something :)

Yeah, that was a weird place for those tests. Moved to a more appropriate location in commit 3.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs
ToddGrun added 2 commits May 14, 2026 20:18
…ed type helpers

Add [CollectionDefinition("SumConverterTypeFiltering", DisableParallelization = true)] to ensure the FirstChanceException-based tests don't observe exceptions from unrelated tests running in parallel.

Extract IsBooleanType, IsStringLikeType, and IsNumericType helpers from the duplicated allowlists in IsTokenCompatibleWithType and IsSerializedAsJsonPrimitiveType. Each type category is now defined in exactly one place, eliminating the risk of the two methods drifting out of sync.
Copilot AI review requested due to automatic review settings May 15, 2026 03:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread src/LanguageServer/ProtocolUnitTests/Converters/SumConverterTypeFilteringTests.cs Outdated
Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs Outdated
 - Cache GetCustomAttribute<JsonConverterAttribute>() results in a ConcurrentDictionary so reflection runs at most once per Type. This matters on hot paths like completion items where thousands of SumType arms are checked per response.
 - Move SumTypeCache and HasCustomJsonConverterCache to the non-generic SumConverter class so they are shared across all SumConverter<T> instantiations instead of duplicated per generic type argument.
 - Scope the FirstChanceException handler in tests to the current thread ID, eliminating false failures from unrelated exceptions on other threads. This removes the need for DisableParallelization.
Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs
Comment thread src/LanguageServer/ProtocolUnitTests/Converters/SumConverterTypeFilteringTests.cs Outdated
/// </summary>
private static bool IsArrayElementCompatible(ref Utf8JsonReader reader, Type arrayType)
{
if (!arrayType.IsArray)
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.

does this represent a bug? (i'm fine with the code as it is, i'm just trying to figure out if this is needed, or just paranoia, or desirable).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is just desirable. Some other razor completion work I'm doing takes in completion items from html/C#/Razor and tries to do completion list promotion, but some of the items aren't compatible, and thus currently require completion level specification of the commit characters. This ends up meaning over 100 exceptions thrown during deserialization, and it slows completion down significantly.

Co-authored-by: Cyrus Najmabadi <cyrus.najmabadi@gmail.com>
Copilot AI review requested due to automatic review settings May 15, 2026 17:09
…peFilteringTests.cs

Co-authored-by: Cyrus Najmabadi <cyrus.najmabadi@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread src/LanguageServer/Protocol/Protocol/Converters/SumConverter.cs Outdated
ToddGrun added 2 commits May 15, 2026 19:11
…tom converters

Replace the single-pass token filtering loop with a two-pass approach:
 - Pass 1 tries only token-compatible arms, avoiding costly exception-based type probing.
 - Pass 2 retries any arms that were skipped by token filtering, as a fallback for types whose converters are registered at the property level or via JsonSerializerOptions rather than as type-level attributes.

This makes the token filtering a best-effort optimization rather than a correctness gate. A wrong prediction from IsTokenCompatibleWithType can no longer cause deserialization failure — it just falls back to the old try/catch behavior for that arm.

As a result, the HasCustomJsonConverterAttribute cache and reflection check are no longer needed and are removed. Extract TryDeserializeArm helper to deduplicate the try/catch logic shared by both passes. Add a test proving the fallback works for a type with an options-registered converter that the token filter doesn't know about.
Copilot AI review requested due to automatic review settings May 16, 2026 02:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

…c types

Add !type.IsArray to the StartObject case so array element types (e.g., int[] as the element type of int[][]) are correctly rejected when the peeked token is a JSON object. Without this, nested array SumType arms like int[][] would pass the StartObject filter and cause an unnecessary deserialization attempt that falls back to the try/catch path.

Add decimal to IsNumericType since System.Text.Json serializes/deserializes it as a JSON number.

Add test Deserialize_ObjectArray_SkipsNestedArrayTypeArm that verifies no internal exceptions are thrown when deserializing SumType<int[][], SimpleObject[]> from JSON containing objects.
@ToddGrun ToddGrun requested a review from dibarbet May 18, 2026 17:48
@ToddGrun ToddGrun merged commit 256895f into dotnet:main May 18, 2026
25 checks passed
@dotnet-policy-service dotnet-policy-service Bot added this to the Next milestone May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants