Skip to content

feat: Phasing out Newtonsoft.Json in favour of System.Text.Json#1046

Merged
nandan-bhat merged 5 commits into
masterfrom
feat/SDK-9900
Jul 9, 2026
Merged

feat: Phasing out Newtonsoft.Json in favour of System.Text.Json#1046
nandan-bhat merged 5 commits into
masterfrom
feat/SDK-9900

Conversation

@kailash-b

@kailash-b kailash-b commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Changes

This PR phases out Newtonsoft.Json in favour of System.Text.Json (STJ) across
Auth0.Core and Auth0.AuthenticationApi, bringing them in line with Auth0.ManagementApi
(already on STJ) and removing the Newtonsoft dependency from the serialization path. The
migration is designed for zero-to-minimum breaking changes for consumers.

Auth0.Core

  • Added Auth0JsonSerializerOptions.Default — a shared internal options singleton
    (PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = WhenWritingNull) that
    reproduces Newtonsoft's case-insensitive matching and NullValueHandling.Ignore.
  • Rewrote ApiErrorConverter as an STJ JsonConverter<ApiError>.
  • Migrated StringOrObjectAsStringConverter and StringOrStringArrayJsonConverter to
    JsonConverter<T>. These converters are internal, so the base-type change is not part
    of the public contract.

Auth0.AuthenticationApi

  • Migrated all model types and HttpClientAuthenticationConnection to STJ
    ([JsonProperty][JsonPropertyName], Newtonsoft [JsonIgnore] → STJ [JsonIgnore],
    serialize/deserialize via the shared options). Serialization uses body.GetType() so
    polymorphic/derived properties are still emitted.

Public API impact (deprecation, not removal)

  • UserInfo.AdditionalClaims (IDictionary<string, JToken>) is kept but marked
    [Obsolete] and [JsonIgnore]. It is now a computed shim that reads from a new
    AdditionalClaimsJson property. Existing consumer code that reads AdditionalClaims
    continues to compile and still returns JTokens.
  • Added UserInfo.AdditionalClaimsJson (IDictionary<string, JsonElement>) as the real
    [JsonExtensionData] sink (additive; defaults to an empty dictionary so the obsolete
    accessor is null-safe).
  • No other public property was renamed, retyped, or removed - all remaining model changes
    are attribute-only.

Intentional behavioural delta

  • StringOrObjectAsStringConverter now emits compact JSON where Newtonsoft emitted
    pretty-printed output (e.g. {"innerValue":"test"} instead of the indented form). This is
    asserted explicitly in the tests.

References

Testing

  • This change adds unit test coverage
  • This change adds integration test coverage
  • This change has been tested on the latest version of the platform/language or why not

Checklist

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.03419% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.06%. Comparing base (66bbeab) to head (7ba9de9).

Files with missing lines Patch % Lines
...th0.AuthenticationApi/FlexibleDateTimeConverter.cs 44.44% 3 Missing and 2 partials ⚠️
...onApi/Serialization/SignupUserResponseConverter.cs 88.23% 3 Missing and 1 partial ⚠️
src/Auth0.Core/Serialization/ApiErrorConverter.cs 90.32% 2 Missing and 1 partial ⚠️
...e/Serialization/StringOrObjectAsStringConverter.cs 85.71% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1046      +/-   ##
==========================================
+ Coverage   32.04%   32.06%   +0.01%     
==========================================
  Files        2802     2803       +1     
  Lines      111115   111147      +32     
  Branches     6492     6495       +3     
==========================================
+ Hits        35607    35637      +30     
- Misses      73643    73647       +4     
+ Partials     1865     1863       -2     
Flag Coverage Δ
authIntTests 3.03% <76.92%> (+0.03%) ⬆️
mgmtIntTests 30.78% <25.64%> (+<0.01%) ⬆️
unittests 0.29% <43.58%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kailash-b
kailash-b marked this pull request as ready for review July 9, 2026 05:14
@kailash-b
kailash-b requested a review from a team as a code owner July 9, 2026 05:14

@nandan-bhat nandan-bhat 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.

Left a few inline comments on the Newtonsoft to System.Text.Json migration. Overall it looks solid and the converter/UserInfo test coverage is good. My main worry is the SignupUserResponse private field handling, plus a couple of spots where the behaviour quietly changes between the two libraries.

[JsonProperty("_id")]
[JsonInclude]
[JsonPropertyName("_id")]
private string _id; // Standard connection

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.

These backing fields are private and rely on [JsonInclude] to get populated. System.Text.Json does not treat [JsonInclude] on private fields the same way Newtonsoft did. Depending on the runtime it either ignores them or throws when it builds the contract. If they get ignored, Id will always be null because none of _id, id or user_id ever get set.

I could not find any test that deserializes SignupUserResponse, so this would slip through. Can we add a test that feeds _id, id and user_id payloads separately and checks Id resolves in each case? If the attribute does not work on net462 / netstandard2.0, we may need the custom TypeInfoResolver approach that ManagementApi already uses, or make these members non private.

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 fixed with a custom serialiser in commit

/// </summary>
[JsonProperty("user_metadata")]
[JsonPropertyName("user_metadata")]
public dynamic UserMetadata { get; set; }

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.

Heads up: UserMetadata has no converter, so under System.Text.Json this deserializes to a JsonElement instead of the JObject Newtonsoft used to give. Any consumer doing dynamic md = response.UserMetadata; md.someField will now throw RuntimeBinderException. We kept the old shape for UserInfo.AdditionalClaims through the obsolete shim, but this one changes silently. At the very least we should list it as a breaking change.

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.

Documented the behaviour. We will also call this out as a breaking change on release.
The field is dynamic which already means there is no guarantee on the type (structure). However, since we are knowingly breaking this type, we can ensure we call this out explicitly.

/// </summary>
[JsonProperty("request_language")]
[JsonPropertyName("request_language")]
public object RequestLanguage { get; set; }

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.

Same story as UserMetadata. This was a JValue under Newtonsoft and becomes a JsonElement now, so a cast like (string)response.RequestLanguage will throw InvalidCastException. Worth calling out as a behaviour change, or exposing it as JsonElement on purpose.

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.

Same as above.

var extraDataProp = props.FirstOrDefault(p => p.PropertyType == typeof(Dictionary<string, string>));

foreach (var jp in JObject.Load(reader).Properties())
foreach (var property in doc.RootElement.EnumerateObject())

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.

EnumerateObject() throws InvalidOperationException when the content is valid JSON but not an object (for example 123, a plain string, or an array). That is not a JsonException, so the try/catch fallback in ApiError.Parse will not catch it and it escapes.

The old Newtonsoft path threw a JsonException here, so the fallback used to kick in and return the raw content. Can we add a guard like if (doc.RootElement.ValueKind != JsonValueKind.Object) and return early, plus a test for something like ApiError.Parse("123")?

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.

Addressed!

case JsonTokenType.Number:
return Add(Epoch, TimeSpan.FromSeconds(reader.GetInt64()));
case JsonTokenType.String:
return reader.GetDateTime();

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.

reader.GetDateTime() only accepts ISO 8601 style strings and throws FormatException on anything else. The old converter was lenient and just returned the raw value. So for any non ISO date string this now throws instead of degrading. Auth0 normally sends epoch or ISO here so the real risk is low, but a DateTime.TryParse fallback (or returning null) would keep the old leniency.

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.

Addressed

Comment thread src/Auth0.Core/Auth0.Core.csproj Outdated
<!-- Package Dependencies -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />

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.

Small consistency thing: Core pins System.Text.Json to 8.0.5 while AuthenticationApi and ManagementApi are on 9.0.9. NuGet will unify to 9.0.9 at runtime so it builds fine, but the different floor here is easy to trip over later, and 8.0.x is a separate servicing line for security patches. Can we bump this to 9.0.9 to match?

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.

Bumped to 9.0.9

namespace Auth0.Core.Serialization;

internal class StringOrStringArrayJsonConverter : JsonConverter
internal class StringOrStringArrayJsonConverter : JsonConverter<object?>

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.

Nit: I could not find any [JsonConverter(typeof(StringOrStringArrayJsonConverter))] usage in src, so this looks unused. If it is dead code we could drop it along with its tests instead of porting it over. Feel free to ignore if it is kept on purpose for something planned.

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.

Deleted dead code

@nandan-bhat
nandan-bhat merged commit 4c3900f into master Jul 9, 2026
10 checks passed
@nandan-bhat
nandan-bhat deleted the feat/SDK-9900 branch July 9, 2026 19:36
@kailash-b kailash-b mentioned this pull request Jul 23, 2026
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.

2 participants