Skip to content

Remove embedded Newtonsoft.Json, migrate to System.Text.Json exclusively#5959

Open
Robbie-Microsoft wants to merge 7 commits intomainfrom
rginsburg/json
Open

Remove embedded Newtonsoft.Json, migrate to System.Text.Json exclusively#5959
Robbie-Microsoft wants to merge 7 commits intomainfrom
rginsburg/json

Conversation

@Robbie-Microsoft
Copy link
Copy Markdown
Contributor

@Robbie-Microsoft Robbie-Microsoft commented Apr 28, 2026

Summary

Removes the embedded Newtonsoft.Json source tree from MSAL.NET and migrates all JSON serialization to System.Text.Json (STJ).

Previously, MSAL.NET bundled a private copy of Newtonsoft.Json (under the Microsoft.Identity.Json namespace) compiled directly into the assembly for all target frameworks except net8.0. This PR eliminates that dependency entirely.

Changes

Deleted

  • src/client/Microsoft.Identity.Client/json/ — the entire embedded Newtonsoft source tree (~100 files)

Infrastructure

  • SUPPORTS_SYSTEM_TEXT_JSON compile flag is now defined unconditionally for all TFMs (was previously only net8.0\netcore)
  • Removed HAVE_* Newtonsoft build-infra DefineConstants from the main csproj
  • Added System.Text.Json NuGet package reference for netstandard2.0, net462, and net472 (it is built into net8.0 and later)
  • The Platforms/net/ STJ helper files (JsonObjectAttribute.cs, JsonStringConverter.cs, MsalJsonSerializerContext.cs) now compile for all TFMs instead of only net8.0

Main library (~44 files)

  • Stripped all #if SUPPORTS_SYSTEM_TEXT_JSON / #else conditional blocks, keeping only the STJ code paths
  • Replaced ArrayBufferWriter<T> (unavailable pre-.NET 5) with MemoryStream in JsonHelper.cs

Platform files

  • Android broker files (BrokerRequest, AndroidBrokerHelper, AndroidAccountManagerBroker, AndroidBrokerInteractiveResponseHelper, AndroidContentProviderBroker): migrated from Microsoft.Identity.Json.Linq to System.Text.Json.Nodes
  • iOS IntuneEnrollmentIdHelper: migrated from JsonConvert.DeserializeObject to JsonSerializer.Deserialize

Tests / Lab

  • ~18 test and lab source files migrated from Newtonsoft APIs (JObject, JToken, JsonConvert, [JsonProperty]) to STJ equivalents (JsonObject, JsonNode, JsonSerializer, [JsonPropertyName])
  • Newtonsoft.Json PackageReference removed from all 9 affected test/lab .csproj files
  • Fixed UnifiedCacheFormatTests: JSON object fields in test data files now use .ToJsonString() instead of .GetValue<string>()

Testing

  • All unit tests pass on both net8.0 and net48 (1910+ passing, 0 failures)
  • Integration tests will run via CI on this PR

Robbie-Microsoft and others added 2 commits April 28, 2026 16:24
…vely

- Delete the embedded Newtonsoft source tree (json/ folder, ~100 files)
- Define SUPPORTS_SYSTEM_TEXT_JSON unconditionally for all TFMs
- Remove HAVE_* Newtonsoft build-infra defines from csproj
- Add System.Text.Json NuGet package for netstandard2.0, net462, net472
- Move Platforms/net STJ helpers to compile for all TFMs
- Strip all #if SUPPORTS_SYSTEM_TEXT_JSON / #else branches from ~44 source files
- Migrate Android broker files from Microsoft.Identity.Json to STJ
- Migrate iOS IntuneEnrollmentIdHelper from Newtonsoft to STJ
- Remove Newtonsoft.Json PackageReference from all test/lab projects
- Migrate ~18 test/lab files from Newtonsoft APIs to STJ equivalents
- Fix ArrayBufferWriter<T> (not available pre-.NET5) with MemoryStream

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
token_response, id_token_response, at_cache_value, id_token_cache_value,
rt_cache_value, account_cache_value are JSON objects in the test data files,
not primitive strings. STJ's GetValue<string>() only works on leaf nodes;
use ToJsonString() to serialize them as strings (matching prior Newtonsoft
JToken.ToString() behavior).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 20:47
@Robbie-Microsoft Robbie-Microsoft requested a review from a team as a code owner April 28, 2026 20:47
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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@bgavrilMS
Copy link
Copy Markdown
Member

Very nice, great to see this @Robbie-Microsoft

Robbie-Microsoft and others added 2 commits April 28, 2026 17:02
- ManagedIdentityTests.NetFwk.cs: remove #if NET8_0_OR_GREATER guard
  around JsonConvert.DeserializeObject; use System.Text.Json.JsonSerializer
  unconditionally (the NetFx project targets net48 where the #else branch
  compiled, but Microsoft.Identity.Json no longer exists)

- AndroidBrokerHelper.cs: replace (string?) nullable cast with
  ?.GetValue<string>() — nullable annotation syntax requires
  #nullable enable context which the Android file does not have (CS8632)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JsonSerializer.Deserialize<T>() without a JsonSerializerContext uses
reflection which is incompatible with iOS Native AOT (IL3050).

Add a local IntuneSerializerContext (partial JsonSerializerContext)
in IntuneEnrollmentIdHelper.cs with [JsonSerializable(typeof(EnrollmentIDs))]
and use the non-generic JsonSerializer.Deserialize(json, type, context)
overload which is AOT-safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 21:17
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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

…ClaimExtractor

- LabResponseHelper.Deserialize<UserConfig/AppConfig>: add PropertyNameCaseInsensitive=true
  to match Newtonsoft's case-insensitive ToObject<T>() behavior. Without this, Key Vault
  JSON properties like 'LabName'/'labName' would not deserialize into UserConfig.LabName,
  causing all integration tests to fail with 'lab name is not set on user'.

- JwtClaimExtractor.TryExtractExpirationClaim: STJ returns JsonElement (not long) when
  deserializing Dictionary<string,object>. Handle JsonElement.TryGetInt64() so expiry is
  correctly extracted from attestation JWTs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gladjohn
Copy link
Copy Markdown
Contributor

Very nice, great to see this @Robbie-Microsoft

Agree

#if SUPPORTS_SYSTEM_TEXT_JSON
using JObject = System.Text.Json.Nodes.JsonObject;
#else
using Microsoft.Identity.Json.Linq;
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.

Do we have enough unit tests / or did we manual test to ensure we are still good in net462/472/netstandard2.0? an nothing is broken?

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.

net462/472/netstandard2.0 aren't targeted by any test project in this repo - that gap predates this PR. The good news is that STJ 6.0.10 itself targets netstandard2.0, so its behavior is consistent across all these TFMs. The net48 unit tests exercise all the same code paths (JsonHelper, cache serialization, IdToken parsing, etc.) and all pass. Happy to add a follow-up PR that adds net462/net472 to the unit test TargetFrameworks if you'd like explicit coverage.

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.

Happy to add a follow-up PR that adds net462/net472 to the unit test TargetFrameworks if you'd like explicit coverage.

I don't think we should do this, but we need a test pass on key features.

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.

Ran the unit tests locally targeting net462 (1,248 passed) and net472 (1,845 passed) by temporarily adding those TFMs — all passing, no JSON-related failures.

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.

I don't think this is what @bgavrilMS suggested to do for the validation.

{
// Output buffer to store the merged JSON
var outputBuffer = new ArrayBufferWriter<byte>();
using var outputStream = new System.IO.MemoryStream();
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.

any concerns with swiching from ArrayBufferWriter to MemoryStream?

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.

No concerns. MemoryStream.ToArray() has a small extra allocation vs ArrayBufferWriter, but Merge() isn't on any hot path. ArrayBufferWriter was only available from .NET 5+, so switching to MemoryStream was required to support net462/472/netstandard2.0.

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.

ArrayBufferWriter actually ships in the System.Memory package and is available down to netstandard2.0 / net462 / net472 (MSAL already pulls System.Memory in transitively)? so it's not a TFM constraint as I can see.

nit - prefer using var outputStream = new MemoryStream(); without the fully-qualified System.IO. since System.IO should already be in usings of that file.

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.

Tried it — ArrayBufferWriter is actually internal in the resolved System.Buffers 4.5.1 package for netstandard2.0/net462/net472, so it doesn't compile on those TFMs. The type only became public in later runtime versions. MemoryStream is the correct choice here for cross-TFM compatibility

@Robbie-Microsoft Robbie-Microsoft requested review from a team and ashok672 April 28, 2026 22:11
@gladjohn
Copy link
Copy Markdown
Contributor

Good PR overall - but we have no test coverage for the net462/472/netstandard2.0 builds of MSAL, so the swap from Newtonsoft to STJ on those TFMs (especially IdToken.Parse claim shapes and cache serialization) needs to be tested.

Adds net462 and net472 as TargetFrameworks to verify System.Text.Json
works correctly on older .NET Framework versions after the Newtonsoft
migration.

Files excluded per TFM due to API availability:
- net462: CertificateRequest/ECCurve not available (added 4.7.1/4.7.2);
  ImdsV2 unsupported (#if NET462 guard in production code)
- net472: WebView2 test (Desktop lib targets net462 only);
  GetCertHash(HashAlgorithmName) not available (added net48)

Results: net462 1248 passed, net472 1845 passed, 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 18:52
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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copy link
Copy Markdown
Contributor

@ashok672 ashok672 left a comment

Choose a reason for hiding this comment

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

Looks good. I tried building locally for all supported TFMs and the build is clean without warning. Thanks for this PR.

@Robbie-Microsoft
Copy link
Copy Markdown
Contributor Author

Robbie-Microsoft commented Apr 30, 2026

Managed to validate the netstandard2.0 code path with actual test execution.

Approach: Created a standalone net6.0 test project (outside the msal-dotnet repo) that references MSAL via ProjectReference. When the consumer targets net6.0, MSBuild selects MSAL's netstandard2.0 binary — exactly the TFM under question. Used MSTest 3.6.3 which ships a native net6.0 adapter.

Tests written and results:

Test Result
TokenResponse_StjDeserialization_ParsesAllFields ✅ Passed
TokenCache_SecondCall_ServedFromCache_NoExtraHttp ✅ Passed
ErrorResponse_StjDeserialization_ThrowsMsalServiceException ✅ Passed

Test Run Successful. Total tests: 3 Passed: 3 Total time: 1.1485 Seconds

Tests exercise:

  • Instance discovery JSON parsing (/common/discovery/instance)
  • OIDC metadata JSON parsing (/.well-known/openid-configuration)
  • OAuth2 token response JSON parsing (including �xt_expires_in,
    ot_before, client_info, scope extra fields handled via JsonExtensionData)
  • OAuth2 error response JSON parsing (�rror, �rror_description, �rror_codes)
  • Token cache round-trip (write + read confirms internal cache serialization works)

All three STJ deserialization paths work correctly on netstandard2.0. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants