From ad8f29dbb6f80d142ceb96298eef1d62e62ee265 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 18:49:40 +0200 Subject: [PATCH] JSON deserialization into a dynamic object: retarget net10.0, add return-type tests Retarget all three projects from net6.0 to net10.0 and move the packages forward: Newtonsoft.Json 13.0.4, Microsoft.NET.Test.Sdk 18.9.0, xunit 2.9.3, xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1, BenchmarkDotNet 0.15.8. Drop three dead package references: - System.Text.Json 6.0.2 in all three projects. It is in-box on net10.0 and pinning 6.0.2 there downgrades the shared-framework assembly. - Microsoft.AspNetCore.Mvc.Testing in the test project. The solution has no web host and no WebApplicationFactory. - BenchmarkDotNet.Diagnostics.Windows in the benchmark project. Unused, and Windows-only in a solution CI builds on Linux. Fix a benchmark that was not comparing like with like. NewtonsoftJsonAnonymousType called UsingAnonymousTypeWithDictionary, which reads two values out of a Dictionary, while SystemTextJsonAnonymousType reads one value out of a nested anonymous type. It now calls UsingAnonymousType, so both benchmarks do the same work. Add ReturnTypeTests, asserting what each API actually returns for an object, an array, a string, a number, a boolean and the null literal. The interesting case: DeserializeObject gives CLR primitives for scalars (string, double, bool), not JValue, and a real null reference for the null literal, while DeserializeObject gives JValue for all of them. Tighten the existing native dynamic test from Assert.ThrowsAny to Assert.Throws, which is the exception member access on a boxed JsonElement actually raises. --- .../Benchmarks.cs | 2 +- ...erializationDynamicObject.Benchmark.csproj | 8 +- ...nDeserializationDynamicObject.Tests.csproj | 14 +- .../NativeJsonUnitTest.cs | 4 +- .../ReturnTypeTests.cs | 136 ++++++++++++++++++ .../JsonDeserializationDynamicObject.csproj | 5 +- 6 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/ReturnTypeTests.cs diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/Benchmarks.cs b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/Benchmarks.cs index 48c7a3372d..7c7f2e5115 100644 --- a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/Benchmarks.cs +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/Benchmarks.cs @@ -51,7 +51,7 @@ public void UsingExpandoObject() [Benchmark] public void NewtonsoftJsonAnonymousType() { - NewtonsoftJson.UsingAnonymousTypeWithDictionary(_json); + NewtonsoftJson.UsingAnonymousType(_json); } [BenchmarkCategory(nameof(NewtonsoftJson))] diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/JsonDeserializationDynamicObject.Benchmark.csproj b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/JsonDeserializationDynamicObject.Benchmark.csproj index 3c1f9a5cc0..7f29b469a1 100644 --- a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/JsonDeserializationDynamicObject.Benchmark.csproj +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Benchmark/JsonDeserializationDynamicObject.Benchmark.csproj @@ -1,6 +1,6 @@  - net6.0 + net10.0 Exe @@ -13,10 +13,8 @@ false - - - - + + diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/JsonDeserializationDynamicObject.Tests.csproj b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/JsonDeserializationDynamicObject.Tests.csproj index 2860d4ca8f..ebec245230 100644 --- a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/JsonDeserializationDynamicObject.Tests.csproj +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/JsonDeserializationDynamicObject.Tests.csproj @@ -1,22 +1,20 @@  - net6.0 + net10.0 enable false - - - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/NativeJsonUnitTest.cs b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/NativeJsonUnitTest.cs index ce6a73333e..733404f2b8 100644 --- a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/NativeJsonUnitTest.cs +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/NativeJsonUnitTest.cs @@ -1,7 +1,7 @@ using JsonDeserializationDynamicObject.Native; using Xunit; using System.Text.Json; -using System; +using Microsoft.CSharp.RuntimeBinder; namespace JsonDeserializationDynamicObject.Tests; @@ -14,7 +14,7 @@ public void GivenJsonString_WhenUsingDynamic_ThenDeserializeAsJsonElement() var dynamicObject = JsonSerializer.Deserialize(jsonString)!; - Assert.ThrowsAny(() => dynamicObject.Genre); + Assert.Throws(() => dynamicObject.Genre); Assert.IsType(dynamicObject); } diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/ReturnTypeTests.cs b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/ReturnTypeTests.cs new file mode 100644 index 0000000000..e1cdfcf359 --- /dev/null +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.Tests/ReturnTypeTests.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; +using Microsoft.CSharp.RuntimeBinder; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace JsonDeserializationDynamicObject.Tests; + +public class ReturnTypeTests +{ + private const string JsonObjectPayload = @"{ ""Genre"": ""Thriller"" }"; + private const string JsonArrayPayload = "[ 1, 2, 3 ]"; + private const string JsonStringPayload = @"""Thriller"""; + private const string JsonNumberPayload = "8.1"; + private const string JsonBooleanPayload = "true"; + private const string JsonNullPayload = "null"; + + [Fact] + public void GivenJsonObject_WhenDeserializingToDynamic_ThenWeGetJObject() + { + object result = JsonConvert.DeserializeObject(JsonObjectPayload)!; + + Assert.IsType(result); + } + + [Fact] + public void GivenJsonArray_WhenDeserializingToDynamic_ThenWeGetJArray() + { + object result = JsonConvert.DeserializeObject(JsonArrayPayload)!; + + Assert.IsType(result); + } + + [Fact] + public void GivenJsonString_WhenDeserializingToDynamic_ThenWeGetString() + { + object result = JsonConvert.DeserializeObject(JsonStringPayload)!; + + Assert.IsType(result); + Assert.Equal("Thriller", result); + } + + [Fact] + public void GivenJsonNumber_WhenDeserializingToDynamic_ThenWeGetDouble() + { + object result = JsonConvert.DeserializeObject(JsonNumberPayload)!; + + Assert.IsType(result); + Assert.Equal(8.1d, result); + } + + [Fact] + public void GivenJsonBoolean_WhenDeserializingToDynamic_ThenWeGetBool() + { + object result = JsonConvert.DeserializeObject(JsonBooleanPayload)!; + + Assert.IsType(result); + Assert.Equal(true, result); + } + + [Fact] + public void GivenJsonNull_WhenDeserializingToDynamic_ThenWeGetNullReference() + { + object? result = JsonConvert.DeserializeObject(JsonNullPayload); + + Assert.Null(result); + } + + [Theory] + [InlineData(JsonStringPayload, JTokenType.String)] + [InlineData(JsonNumberPayload, JTokenType.Float)] + [InlineData(JsonBooleanPayload, JTokenType.Boolean)] + [InlineData(JsonNullPayload, JTokenType.Null)] + public void GivenJsonScalar_WhenDeserializingToJToken_ThenWeGetJValueOfExpectedType(string payload, JTokenType expected) + { + var result = JsonConvert.DeserializeObject(payload)!; + + var value = Assert.IsType(result); + Assert.Equal(expected, value.Type); + } + + [Fact] + public void GivenJsonObject_WhenDeserializingToJToken_ThenWeGetJObject() + { + var result = JsonConvert.DeserializeObject(JsonObjectPayload)!; + + Assert.IsType(result); + } + + [Fact] + public void GivenJsonArray_WhenDeserializingToJToken_ThenWeGetJArray() + { + var result = JsonConvert.DeserializeObject(JsonArrayPayload)!; + + Assert.IsType(result); + } + + [Theory] + [InlineData(JsonObjectPayload, JsonValueKind.Object)] + [InlineData(JsonArrayPayload, JsonValueKind.Array)] + [InlineData(JsonStringPayload, JsonValueKind.String)] + [InlineData(JsonNumberPayload, JsonValueKind.Number)] + [InlineData(JsonBooleanPayload, JsonValueKind.True)] + public void GivenJsonPayload_WhenDeserializingToDynamicWithSystemTextJson_ThenWeGetJsonElementOfExpectedKind(string payload, JsonValueKind expected) + { + object result = JsonSerializer.Deserialize(payload)!; + + var element = Assert.IsType(result); + Assert.Equal(expected, element.ValueKind); + } + + [Fact] + public void GivenJsonNull_WhenDeserializingToDynamicWithSystemTextJson_ThenWeGetNullReference() + { + object? result = JsonSerializer.Deserialize(JsonNullPayload); + + Assert.Null(result); + } + + [Fact] + public void GivenJsonElement_WhenAccessingMemberByName_ThenRuntimeBinderExceptionIsThrown() + { + dynamic result = JsonSerializer.Deserialize(JsonObjectPayload)!; + + Assert.Throws(() => result.Genre); + } + + [Fact] + public void GivenJObject_WhenAccessingMemberByName_ThenValueIsResolvedAtRuntime() + { + dynamic result = JsonConvert.DeserializeObject(JsonObjectPayload)!; + + Assert.Equal("Thriller", (string)result.Genre); + } +} diff --git a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.csproj b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.csproj index 2b13dda96c..d2d525a8c4 100644 --- a/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.csproj +++ b/json-csharp/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject/JsonDeserializationDynamicObject.csproj @@ -1,14 +1,13 @@ - net6.0 + net10.0 enable enable - - +