diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 75d8cdd978171c..9fb4433e743bde 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -277,10 +277,12 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada string typeCompilableName = typeMetadata.TypeRef; string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - StringBuilder sb = new(); + string metadataInitSource; // TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper. - string metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; + if (typeMetadata.IsValueType) + { + metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; {TypeTypeRef} typeToConvert = typeof({typeCompilableName}); if (!converter.CanConvert(typeToConvert)) {{ @@ -309,6 +311,18 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada }} _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + } + else + { + metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; + {TypeTypeRef} typeToConvert = typeof({typeCompilableName}); + if (!converter.CanConvert(typeToConvert)) + {{ + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); + }} + + _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + } return GenerateForType(typeMetadata, metadataInitSource); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index 72af4d22fe3faa..dde4ae8b8bd398 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -29,6 +29,10 @@ public interface ITestContext public JsonTypeInfo ObjectArray { get; } public JsonTypeInfo String { get; } public JsonTypeInfo ClassWithEnumAndNullable { get; } + public JsonTypeInfo ClassWithCustomConverter { get; } + public JsonTypeInfo StructWithCustomConverter { get; } + public JsonTypeInfo ClassWithBadCustomConverter { get; } + public JsonTypeInfo StructWithBadCustomConverter { get; } } internal partial class JsonContext : JsonSerializerContext diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs index c924e5af8ccf37..990b19bf290a24 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -27,6 +27,10 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(object[]))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(ClassWithCustomConverter))] + [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithBadCustomConverter))] + [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext { } @@ -58,6 +62,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.Serialize); Assert.Null(MetadataAndSerializationContext.Default.String.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverter); + Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverter); + Assert.Throws(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter); + Assert.Throws(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs index e6d1fd754f10aa..2fa7bf35a099fe 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -26,6 +26,12 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { } @@ -55,6 +61,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataWithPerTypeAttributeContext.Default.ObjectArray.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.String.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize); + Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize); + Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize); } } @@ -79,6 +89,10 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(object[]))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(ClassWithCustomConverter))] + [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithBadCustomConverter))] + [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class MetadataContext : JsonSerializerContext, ITestContext { } @@ -110,6 +124,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.ObjectArray.Serialize); Assert.Null(MetadataContext.Default.String.Serialize); Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.Null(MetadataContext.Default.ClassWithCustomConverter.Serialize); + Assert.Null(MetadataContext.Default.StructWithCustomConverter.Serialize); + Assert.Throws(() => MetadataContext.Default.ClassWithBadCustomConverter.Serialize); + Assert.Throws(() => MetadataContext.Default.StructWithBadCustomConverter.Serialize); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index 64371e837de2ab..2fb7dd3ec63d28 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -26,6 +26,10 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] internal partial class MixedModeContext : JsonSerializerContext, ITestContext { } @@ -56,6 +60,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MixedModeContext.Default.ObjectArray.Serialize); Assert.Null(MixedModeContext.Default.String.Serialize); Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.Null(MixedModeContext.Default.ClassWithCustomConverter.Serialize); + Assert.Null(MixedModeContext.Default.StructWithCustomConverter.Serialize); + Assert.Throws(() => MixedModeContext.Default.ClassWithBadCustomConverter.Serialize); + Assert.Throws(() => MixedModeContext.Default.StructWithBadCustomConverter.Serialize); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index ca941bea915903..61c04c0c3acf70 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -110,6 +110,64 @@ public virtual void RoundTripTypeNameClash() VerifyRepeatedLocation(expected, obj); } + [Fact] + public virtual void RoundTripWithCustomConverter_Class() + { + const string Json = "{\"MyInt\":142}"; + + ClassWithCustomConverter obj = new ClassWithCustomConverter() + { + MyInt = 42 + }; + + string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverter); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverter); + Assert.Equal(42, obj.MyInt); + } + + [Fact] + public virtual void RoundTripWithCustomConverter_Struct() + { + const string Json = "{\"MyInt\":142}"; + + StructWithCustomConverter obj = new StructWithCustomConverter() + { + MyInt = 42 + }; + + string json = JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverter); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, DefaultContext.StructWithCustomConverter); + Assert.Equal(42, obj.MyInt); + } + + [Fact] + public virtual void BadCustomConverter_Class() + { + const string Json = "{\"MyInt\":142}"; + + Assert.Throws(() => + JsonSerializer.Serialize(new ClassWithBadCustomConverter(), DefaultContext.ClassWithBadCustomConverter)); + + Assert.Throws(() => + JsonSerializer.Deserialize(Json, DefaultContext.ClassWithBadCustomConverter)); + } + + [Fact] + public virtual void BadCustomConverter_Struct() + { + const string Json = "{\"MyInt\":142}"; + + Assert.Throws(() => + JsonSerializer.Serialize(new StructWithBadCustomConverter(), DefaultContext.StructWithBadCustomConverter)); + + Assert.Throws(() => + JsonSerializer.Deserialize(Json, DefaultContext.StructWithBadCustomConverter)); + } + protected static Location CreateLocation() { return new Location diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index 799de2b1032108..797b250cb70293 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -27,6 +27,10 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(object[]))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(ClassWithCustomConverter))] + [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithBadCustomConverter))] + [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class SerializationContext : JsonSerializerContext, ITestContext { } @@ -51,6 +55,12 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { } @@ -76,6 +86,10 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext { } @@ -112,6 +126,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(SerializationContext.Default.ObjectArray.Serialize); Assert.Null(SerializationContext.Default.String.Serialize); Assert.NotNull(SerializationContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.Null(SerializationContext.Default.ClassWithCustomConverter.Serialize); + Assert.Null(SerializationContext.Default.StructWithCustomConverter.Serialize); + Assert.Throws(() => SerializationContext.Default.ClassWithBadCustomConverter.Serialize); + Assert.Throws(() => SerializationContext.Default.StructWithBadCustomConverter.Serialize); } [Fact] @@ -370,6 +388,10 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(SerializationWithPerTypeAttributeContext.Default.ObjectArray.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.String.Serialize); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize); + Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize); + Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index 887f65b80da83b..4a6123f8733cf4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -151,4 +151,110 @@ public class JsonMessage } internal struct MyStruct { } + + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverterForClass : JsonConverter + { + public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + ClassWithCustomConverter obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + [JsonConverter(typeof(CustomConverterForClass))] + public class ClassWithCustomConverter + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverterForStruct))] // Invalid + public struct ClassWithBadCustomConverter + { + public int MyInt { get; set; } + } + + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverterForStruct : JsonConverter + { + public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + StructWithCustomConverter obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + [JsonConverter(typeof(CustomConverterForStruct))] + public struct StructWithCustomConverter + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverterForClass))] // Invalid + public struct StructWithBadCustomConverter + { + public int MyInt { get; set; } + } }