From f2e91d1c85e2a45347649b76f6c3a92e8bfffbbf Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 18:57:17 +0200 Subject: [PATCH] Global JSON options: retarget net10.0, configure Newtonsoft through MVC Retarget both projects to net10.0 and lift the packages: Newtonsoft.Json 13.0.4, Microsoft.AspNetCore.Mvc.Testing 10.0.11, Microsoft.NET.Test.Sdk 18.9.0, MSTest 4.3.3, coverlet.collector 10.0.1. Configure MVC's JSON options through AddControllers().AddJsonOptions() instead of Configure(), which needed the reader to know which of the two JsonOptions types the using directive brought into scope. Replace the JsonConvert.DefaultSettings sample with AddControllers().AddNewtonsoftJson(). DefaultSettings only governs our own JsonConvert calls and never reaches the MVC response pipeline, so the old sample taught per-call serialization under a global-configuration heading. SaveProduct now returns the object and lets the formatter write it. The Newtonsoft registration sits behind a UseNewtonsoftJson configuration flag because it replaces both MVC formatters, and the sample still has to show the System.Text.Json path. Drop the Swashbuckle wiring: the .NET 10 web API template no longer ships it and the article never mentions Swagger. Drop MaxDepth = 3. The sample payload is two levels deep, so the setting demonstrates nothing and breaks anyone who adds a nesting level. Add integration tests covering all three: System.Text.Json options on a controller action, Json.NET writing the response once AddNewtonsoftJson() is registered, and the minimal API endpoint staying on System.Text.Json either way. --- .../Controllers/ProductController.cs | 3 +- ...obalDefaultJsonSerializationoptions.csproj | 6 +- .../Program.cs | 61 ++++++------- ...ltJsonSerializationoptionsUnitTests.csproj | 12 +-- .../GlobalJsonOptionsIntegrationTests.cs | 90 +++++++++++++++++++ .../ProductControllerUnitTests.cs | 5 +- 6 files changed, 130 insertions(+), 47 deletions(-) create mode 100644 json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs index 6ce51f034a..188b7693d2 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; namespace GlobalDefaultJsonSerializationoptions.Controllers; @@ -16,6 +15,6 @@ public ActionResult CreateProduct(Product product) [HttpPost("save")] public ActionResult SaveProduct(Product product) { - return Ok(JsonConvert.SerializeObject(product)); + return Ok(product); } } diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj index c7cc13203b..703f2e6bb7 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj @@ -1,15 +1,15 @@ - net8.0 + net10.0 enable enable true - - + + diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs index 0397cc9e1d..adc6baf8a5 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs @@ -1,27 +1,22 @@ -using Microsoft.AspNetCore.Mvc; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using GlobalDefaultJsonSerializationoptions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using GlobalDefaultJsonSerializationoptions; -using System.Text.Encodings.Web; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddControllers(); - -builder.Services.Configure(options => -{ - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.JsonSerializerOptions.WriteIndented = false; - options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default; - options.JsonSerializerOptions.AllowTrailingCommas = true; - options.JsonSerializerOptions.MaxDepth = 3; - options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; -}); +var controllers = builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.WriteIndented = false; + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default; + options.JsonSerializerOptions.AllowTrailingCommas = true; + options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; + }); builder.Services.ConfigureHttpJsonOptions(options => { @@ -30,28 +25,28 @@ options.SerializerOptions.WriteIndented = false; options.SerializerOptions.Encoder = JavaScriptEncoder.Default; options.SerializerOptions.AllowTrailingCommas = true; - options.SerializerOptions.MaxDepth = 3; options.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; }); -JsonConvert.DefaultSettings = () => new JsonSerializerSettings +// AddNewtonsoftJson() replaces both MVC formatters, so it would take the +// System.Text.Json configuration above out of play for every controller action. +// The sample demonstrates all three approaches, so this one is registered behind +// a configuration switch: run with UseNewtonsoftJson=true to serialize controller +// responses with Json.NET instead. +if (builder.Configuration.GetValue("UseNewtonsoftJson")) { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DateFormatString = "dd-MM-yyyy", - DefaultValueHandling = DefaultValueHandling.Ignore, - MaxDepth = 3 -}; + controllers.AddNewtonsoftJson(options => + { + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.DateFormatString = "dd-MM-yyyy"; + options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore; + }); +} var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.UseHttpsRedirection(); app.UseAuthorization(); @@ -64,3 +59,5 @@ }); app.Run(); + +public partial class Program; diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj index f0985e8813..4522c9d726 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs new file mode 100644 index 0000000000..3ded8c4eeb --- /dev/null +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs @@ -0,0 +1,90 @@ +using System.Text; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GlobalDefaultJsonSerializationoptionsUnitTests; + +[TestClass] +public class GlobalJsonOptionsIntegrationTests +{ + private const string RequestBody = """ + { + "Id": 1, + "Name": null, + "Price": 0, + "Quantity": "5", + "ReleaseDate": "2024-04-14T10:49:31.813Z", + "Manufacturer": { "Name": "Apple", "Location": "California" } + } + """; + + private static HttpContent JsonContent() => + new StringContent(RequestBody, Encoding.UTF8, "application/json"); + + private static WebApplicationFactory CreateFactory(bool useNewtonsoftJson) => + new WebApplicationFactory() + .WithWebHostBuilder(builder => + builder.UseSetting("UseNewtonsoftJson", useNewtonsoftJson.ToString())); + + private static async Task PostAsync(HttpClient client, string route) + { + var response = await client.PostAsync(route, JsonContent()); + var body = await response.Content.ReadAsStringAsync(); + + Assert.IsTrue(response.IsSuccessStatusCode, body); + + return JsonNode.Parse(body)!.AsObject(); + } + + [TestMethod] + public async Task GivenSystemTextJsonOptions_WhenControllerActionResponds_ThenItAppliesTheConfiguredOptions() + { + using var factory = CreateFactory(useNewtonsoftJson: false); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product"); + + // Camel casing, nulls dropped, and a number read from a string. + CollectionAssert.AreEquivalent( + new[] { "id", "price", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreEqual(5, (int)payload["quantity"]!); + Assert.AreEqual("Apple", (string?)payload["manufacturer"]!["name"]); + + // The Newtonsoft date format is not registered on this run. + Assert.AreNotEqual("14-04-2024", (string?)payload["releaseDate"]); + } + + [TestMethod] + public async Task GivenNewtonsoftJsonIsRegistered_WhenControllerActionResponds_ThenJsonNetWritesTheResponse() + { + using var factory = CreateFactory(useNewtonsoftJson: true); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product/save"); + + // AddNewtonsoftJson() replaced the MVC formatters: Json.NET's date format + // and DefaultValueHandling.Ignore are both visible in the response. + CollectionAssert.AreEquivalent( + new[] { "id", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreEqual("14-04-2024", (string?)payload["releaseDate"]); + } + + [TestMethod] + public async Task GivenNewtonsoftJsonIsRegistered_WhenMinimalApiEndpointResponds_ThenHttpJsonOptionsStillApply() + { + using var factory = CreateFactory(useNewtonsoftJson: true); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product/create"); + + // MVC options and Http JSON options do not cascade: the minimal API + // endpoint keeps serializing with System.Text.Json. + CollectionAssert.AreEquivalent( + new[] { "id", "price", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreNotEqual("14-04-2024", (string?)payload["releaseDate"]); + } +} diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs index d49a23d6e6..f803382284 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs @@ -2,7 +2,6 @@ using GlobalDefaultJsonSerializationoptions.Controllers; using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace GlobalDefaultJsonSerializationoptionsUnitTests; @@ -42,8 +41,6 @@ public void GivenValidProduct_WhenSaveProductIsCalled_ThenReturnsProductWithSame // Assert Assert.IsNotNull(result); Assert.AreEqual(200, result.StatusCode); - - var expectedJson = JsonConvert.SerializeObject(product); - Assert.AreEqual(expectedJson, result?.Value?.ToString()); + Assert.AreEqual(product, result.Value); } }