Skip to content

Commit 3915396

Browse files
authored
Fix JsonException message property type info in class parameterized constructor (#126575)
This pull request fixes an **incorrect type hint** in the thrown `JsonException` when deserialization fails for **classes with parameterized constructors**. ## Reproduction ```csharp using System.Text.Json; Test<NormalClass>(); Test<ParameterizedNormalClass>(); Test<RecordClass>(); Test<NormalStruct>(); Test<ParameterizedNormalStruct>(); Test<RecordStruct>(); static void Test<T>() { try { JsonSerializer.Deserialize<T>("{\"Text\":{}}"); // create json convert exception manually } catch (Exception e) { Console.WriteLine(e.Message); } } class NormalClass { public string Text { get; set; } = null!; } class ParameterizedNormalClass(string text) { public string Text { get; set; } = text; } record RecordClass(string Text); struct NormalStruct() { public string Text { get; set; } = null!; } struct ParameterizedNormalStruct(string text) { public string Text { get; set; } = text; } record struct RecordStruct(string Text); ``` ## Expected Output ```txt The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. ``` ## Actual Output ```txt The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to ParameterizedNormalClass. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to RecordClass. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. The JSON value could not be converted to System.String. Path: $.Text | LineNumber: 0 | BytePositionInLine: 9. ``` ## Root Cause For classes with parameterized constructors, `state.Current.CtorArgumentState.JsonParameterInfo` was used instead of `state.Current.JsonPropertyInfo`. (All structs, including parameterized structs and record structs, work correctly without this issue.) https://github.com/dotnet/runtime/blob/3f14c31b74e2324a72aec383e92b9aabf65d1f22/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs#L636-L642 As a result, the exception message fell back to `state.Current.JsonTypeInfo.Type` for affected classes. https://github.com/dotnet/runtime/blob/3f14c31b74e2324a72aec383e92b9aabf65d1f22/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs#L542-L544 ## Fix Added logic to retrieve type information from `state.Current.CtorArgumentState.JsonParameterInfo.ParameterType`, ensuring correct type resolution for classes with parameterized constructors.
1 parent 17f52df commit 3915396

File tree

3 files changed

+33
-1
lines changed

3 files changed

+33
-1
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ public static void AddJsonExceptionInformation(scoped ref ReadStack state, in Ut
540540
if (string.IsNullOrEmpty(message))
541541
{
542542
// Use a default message.
543-
Type propertyType = state.Current.JsonPropertyInfo?.PropertyType ?? state.Current.JsonTypeInfo.Type;
543+
Type propertyType = state.Current.JsonPropertyInfo?.PropertyType ??
544+
state.Current.CtorArgumentState?.JsonParameterInfo?.ParameterType ??
545+
state.Current.JsonTypeInfo.Type;
544546
message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType);
545547
ex.AppendPathInformation = true;
546548
}

src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,5 +350,31 @@ public ClassWithInvalidDictionary(Dictionary<string, int[,]> unsupportedDictiona
350350
UnsupportedDictionary = unsupportedDictionary;
351351
}
352352
}
353+
354+
[Fact]
355+
public async Task ParameterizedConstructor_ExceptionMessage_ReportsPropertyType()
356+
{
357+
// Regression test: classes with parameterized constructors should report the
358+
// property type (System.String), not the declaring class type, in the JsonException message.
359+
// https://github.com/dotnet/runtime/pull/126575
360+
JsonException e;
361+
362+
e = await Assert.ThrowsAsync<JsonException>(() =>
363+
Serializer.DeserializeWrapper<ParameterizedClass_WithStringProperty>("""{"Text":{}}"""));
364+
Assert.Contains("System.String", e.Message);
365+
Assert.DoesNotContain(nameof(ParameterizedClass_WithStringProperty), e.Message);
366+
367+
e = await Assert.ThrowsAsync<JsonException>(() =>
368+
Serializer.DeserializeWrapper<ParameterizedRecord_WithStringProperty>("""{"Text":{}}"""));
369+
Assert.Contains("System.String", e.Message);
370+
Assert.DoesNotContain(nameof(ParameterizedRecord_WithStringProperty), e.Message);
371+
}
372+
373+
public class ParameterizedClass_WithStringProperty(string text)
374+
{
375+
public string Text { get; set; } = text;
376+
}
377+
378+
public record ParameterizedRecord_WithStringProperty(string Text);
353379
}
354380
}

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper)
177177
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))]
178178
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))]
179179
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))]
180+
[JsonSerializable(typeof(ParameterizedClass_WithStringProperty))]
181+
[JsonSerializable(typeof(ParameterizedRecord_WithStringProperty))]
180182
internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext
181183
{
182184
}
@@ -349,6 +351,8 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso
349351
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))]
350352
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))]
351353
[JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))]
354+
[JsonSerializable(typeof(ParameterizedClass_WithStringProperty))]
355+
[JsonSerializable(typeof(ParameterizedRecord_WithStringProperty))]
352356
internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext
353357
{
354358
}

0 commit comments

Comments
 (0)