Skip to content

Commit 97d8130

Browse files
authored
Support reference type custom converters (dotnet#57592)
1 parent 44e2cf3 commit 97d8130

File tree

8 files changed

+240
-2
lines changed

8 files changed

+240
-2
lines changed

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

+16-2
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,12 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada
277277
string typeCompilableName = typeMetadata.TypeRef;
278278
string typeFriendlyName = typeMetadata.TypeInfoPropertyName;
279279

280-
StringBuilder sb = new();
280+
string metadataInitSource;
281281

282282
// TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper.
283-
string metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
283+
if (typeMetadata.IsValueType)
284+
{
285+
metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
284286
{TypeTypeRef} typeToConvert = typeof({typeCompilableName});
285287
if (!converter.CanConvert(typeToConvert))
286288
{{
@@ -309,6 +311,18 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada
309311
}}
310312
311313
_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);";
314+
}
315+
else
316+
{
317+
metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
318+
{TypeTypeRef} typeToConvert = typeof({typeCompilableName});
319+
if (!converter.CanConvert(typeToConvert))
320+
{{
321+
throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'."");
322+
}}
323+
324+
_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);";
325+
}
312326

313327
return GenerateForType(typeMetadata, metadataInitSource);
314328
}

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public interface ITestContext
2929
public JsonTypeInfo<object[]> ObjectArray { get; }
3030
public JsonTypeInfo<string> String { get; }
3131
public JsonTypeInfo<RealWorldContextTests.ClassWithEnumAndNullable> ClassWithEnumAndNullable { get; }
32+
public JsonTypeInfo<ClassWithCustomConverter> ClassWithCustomConverter { get; }
33+
public JsonTypeInfo<StructWithCustomConverter> StructWithCustomConverter { get; }
34+
public JsonTypeInfo<ClassWithBadCustomConverter> ClassWithBadCustomConverter { get; }
35+
public JsonTypeInfo<StructWithBadCustomConverter> StructWithBadCustomConverter { get; }
3236
}
3337

3438
internal partial class JsonContext : JsonSerializerContext

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ namespace System.Text.Json.SourceGeneration.Tests
2727
[JsonSerializable(typeof(object[]))]
2828
[JsonSerializable(typeof(string))]
2929
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
30+
[JsonSerializable(typeof(ClassWithCustomConverter))]
31+
[JsonSerializable(typeof(StructWithCustomConverter))]
32+
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
33+
[JsonSerializable(typeof(StructWithBadCustomConverter))]
3034
internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext
3135
{
3236
}
@@ -58,6 +62,10 @@ public override void EnsureFastPathGeneratedAsExpected()
5862
Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.Serialize);
5963
Assert.Null(MetadataAndSerializationContext.Default.String.Serialize);
6064
Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.Serialize);
65+
Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverter);
66+
Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverter);
67+
Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter);
68+
Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter);
6169
}
6270
}
6371
}

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ namespace System.Text.Json.SourceGeneration.Tests
2626
[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)]
2727
[JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata)]
2828
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)]
29+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
30+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
31+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
32+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
33+
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
34+
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
2935
internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
3036
{
3137
}
@@ -55,6 +61,10 @@ public override void EnsureFastPathGeneratedAsExpected()
5561
Assert.Null(MetadataWithPerTypeAttributeContext.Default.ObjectArray.Serialize);
5662
Assert.Null(MetadataWithPerTypeAttributeContext.Default.String.Serialize);
5763
Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize);
64+
Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize);
65+
Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize);
66+
Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize);
67+
Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize);
5868
}
5969
}
6070

@@ -79,6 +89,10 @@ public override void EnsureFastPathGeneratedAsExpected()
7989
[JsonSerializable(typeof(object[]))]
8090
[JsonSerializable(typeof(string))]
8191
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
92+
[JsonSerializable(typeof(ClassWithCustomConverter))]
93+
[JsonSerializable(typeof(StructWithCustomConverter))]
94+
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
95+
[JsonSerializable(typeof(StructWithBadCustomConverter))]
8296
internal partial class MetadataContext : JsonSerializerContext, ITestContext
8397
{
8498
}
@@ -110,6 +124,10 @@ public override void EnsureFastPathGeneratedAsExpected()
110124
Assert.Null(MetadataContext.Default.ObjectArray.Serialize);
111125
Assert.Null(MetadataContext.Default.String.Serialize);
112126
Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.Serialize);
127+
Assert.Null(MetadataContext.Default.ClassWithCustomConverter.Serialize);
128+
Assert.Null(MetadataContext.Default.StructWithCustomConverter.Serialize);
129+
Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.ClassWithBadCustomConverter.Serialize);
130+
Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.StructWithBadCustomConverter.Serialize);
113131
}
114132
}
115133
}

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ namespace System.Text.Json.SourceGeneration.Tests
2626
[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)]
2727
[JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
2828
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
29+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
30+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
31+
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
32+
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
2933
internal partial class MixedModeContext : JsonSerializerContext, ITestContext
3034
{
3135
}
@@ -56,6 +60,10 @@ public override void EnsureFastPathGeneratedAsExpected()
5660
Assert.Null(MixedModeContext.Default.ObjectArray.Serialize);
5761
Assert.Null(MixedModeContext.Default.String.Serialize);
5862
Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.Serialize);
63+
Assert.Null(MixedModeContext.Default.ClassWithCustomConverter.Serialize);
64+
Assert.Null(MixedModeContext.Default.StructWithCustomConverter.Serialize);
65+
Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.ClassWithBadCustomConverter.Serialize);
66+
Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.StructWithBadCustomConverter.Serialize);
5967
}
6068

6169
[Fact]

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs

+58
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,64 @@ public virtual void RoundTripTypeNameClash()
110110
VerifyRepeatedLocation(expected, obj);
111111
}
112112

113+
[Fact]
114+
public virtual void RoundTripWithCustomConverter_Class()
115+
{
116+
const string Json = "{\"MyInt\":142}";
117+
118+
ClassWithCustomConverter obj = new ClassWithCustomConverter()
119+
{
120+
MyInt = 42
121+
};
122+
123+
string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverter);
124+
Assert.Equal(Json, json);
125+
126+
obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverter);
127+
Assert.Equal(42, obj.MyInt);
128+
}
129+
130+
[Fact]
131+
public virtual void RoundTripWithCustomConverter_Struct()
132+
{
133+
const string Json = "{\"MyInt\":142}";
134+
135+
StructWithCustomConverter obj = new StructWithCustomConverter()
136+
{
137+
MyInt = 42
138+
};
139+
140+
string json = JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverter);
141+
Assert.Equal(Json, json);
142+
143+
obj = JsonSerializer.Deserialize(Json, DefaultContext.StructWithCustomConverter);
144+
Assert.Equal(42, obj.MyInt);
145+
}
146+
147+
[Fact]
148+
public virtual void BadCustomConverter_Class()
149+
{
150+
const string Json = "{\"MyInt\":142}";
151+
152+
Assert.Throws<InvalidOperationException>(() =>
153+
JsonSerializer.Serialize(new ClassWithBadCustomConverter(), DefaultContext.ClassWithBadCustomConverter));
154+
155+
Assert.Throws<InvalidOperationException>(() =>
156+
JsonSerializer.Deserialize(Json, DefaultContext.ClassWithBadCustomConverter));
157+
}
158+
159+
[Fact]
160+
public virtual void BadCustomConverter_Struct()
161+
{
162+
const string Json = "{\"MyInt\":142}";
163+
164+
Assert.Throws<InvalidOperationException>(() =>
165+
JsonSerializer.Serialize(new StructWithBadCustomConverter(), DefaultContext.StructWithBadCustomConverter));
166+
167+
Assert.Throws<InvalidOperationException>(() =>
168+
JsonSerializer.Deserialize(Json, DefaultContext.StructWithBadCustomConverter));
169+
}
170+
113171
protected static Location CreateLocation()
114172
{
115173
return new Location

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs

+22
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ namespace System.Text.Json.SourceGeneration.Tests
2727
[JsonSerializable(typeof(object[]))]
2828
[JsonSerializable(typeof(string))]
2929
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
30+
[JsonSerializable(typeof(ClassWithCustomConverter))]
31+
[JsonSerializable(typeof(StructWithCustomConverter))]
32+
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
33+
[JsonSerializable(typeof(StructWithBadCustomConverter))]
3034
internal partial class SerializationContext : JsonSerializerContext, ITestContext
3135
{
3236
}
@@ -51,6 +55,12 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex
5155
[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)]
5256
[JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)]
5357
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
58+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
59+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
60+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
61+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
62+
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
63+
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
5464
internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
5565
{
5666
}
@@ -76,6 +86,10 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer
7686
[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)]
7787
[JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)]
7888
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
89+
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
90+
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
91+
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
92+
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
7993
internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext
8094
{
8195
}
@@ -112,6 +126,10 @@ public override void EnsureFastPathGeneratedAsExpected()
112126
Assert.Null(SerializationContext.Default.ObjectArray.Serialize);
113127
Assert.Null(SerializationContext.Default.String.Serialize);
114128
Assert.NotNull(SerializationContext.Default.ClassWithEnumAndNullable.Serialize);
129+
Assert.Null(SerializationContext.Default.ClassWithCustomConverter.Serialize);
130+
Assert.Null(SerializationContext.Default.StructWithCustomConverter.Serialize);
131+
Assert.Throws<InvalidOperationException>(() => SerializationContext.Default.ClassWithBadCustomConverter.Serialize);
132+
Assert.Throws<InvalidOperationException>(() => SerializationContext.Default.StructWithBadCustomConverter.Serialize);
115133
}
116134

117135
[Fact]
@@ -370,6 +388,10 @@ public override void EnsureFastPathGeneratedAsExpected()
370388
Assert.Null(SerializationWithPerTypeAttributeContext.Default.ObjectArray.Serialize);
371389
Assert.Null(SerializationWithPerTypeAttributeContext.Default.String.Serialize);
372390
Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize);
391+
Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize);
392+
Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize);
393+
Assert.Throws<InvalidOperationException>(() => SerializationWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize);
394+
Assert.Throws<InvalidOperationException>(() => SerializationWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize);
373395
}
374396
}
375397
}

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs

+106
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,110 @@ public class JsonMessage
151151
}
152152

153153
internal struct MyStruct { }
154+
155+
/// <summary>
156+
/// Custom converter that adds\substract 100 from MyIntProperty.
157+
/// </summary>
158+
public class CustomConverterForClass : JsonConverter<ClassWithCustomConverter>
159+
{
160+
public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
161+
{
162+
if (reader.TokenType != JsonTokenType.StartObject)
163+
{
164+
throw new JsonException("No StartObject");
165+
}
166+
167+
ClassWithCustomConverter obj = new();
168+
169+
reader.Read();
170+
if (reader.TokenType != JsonTokenType.PropertyName &&
171+
reader.GetString() != "MyInt")
172+
{
173+
throw new JsonException("Wrong property name");
174+
}
175+
176+
reader.Read();
177+
obj.MyInt = reader.GetInt32() - 100;
178+
179+
reader.Read();
180+
if (reader.TokenType != JsonTokenType.EndObject)
181+
{
182+
throw new JsonException("No EndObject");
183+
}
184+
185+
return obj;
186+
}
187+
188+
public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options)
189+
{
190+
writer.WriteStartObject();
191+
writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100);
192+
writer.WriteEndObject();
193+
}
194+
}
195+
196+
[JsonConverter(typeof(CustomConverterForClass))]
197+
public class ClassWithCustomConverter
198+
{
199+
public int MyInt { get; set; }
200+
}
201+
202+
[JsonConverter(typeof(CustomConverterForStruct))] // Invalid
203+
public struct ClassWithBadCustomConverter
204+
{
205+
public int MyInt { get; set; }
206+
}
207+
208+
/// <summary>
209+
/// Custom converter that adds\substract 100 from MyIntProperty.
210+
/// </summary>
211+
public class CustomConverterForStruct : JsonConverter<StructWithCustomConverter>
212+
{
213+
public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
214+
{
215+
if (reader.TokenType != JsonTokenType.StartObject)
216+
{
217+
throw new JsonException("No StartObject");
218+
}
219+
220+
StructWithCustomConverter obj = new();
221+
222+
reader.Read();
223+
if (reader.TokenType != JsonTokenType.PropertyName &&
224+
reader.GetString() != "MyInt")
225+
{
226+
throw new JsonException("Wrong property name");
227+
}
228+
229+
reader.Read();
230+
obj.MyInt = reader.GetInt32() - 100;
231+
232+
reader.Read();
233+
if (reader.TokenType != JsonTokenType.EndObject)
234+
{
235+
throw new JsonException("No EndObject");
236+
}
237+
238+
return obj;
239+
}
240+
241+
public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options)
242+
{
243+
writer.WriteStartObject();
244+
writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100);
245+
writer.WriteEndObject();
246+
}
247+
}
248+
249+
[JsonConverter(typeof(CustomConverterForStruct))]
250+
public struct StructWithCustomConverter
251+
{
252+
public int MyInt { get; set; }
253+
}
254+
255+
[JsonConverter(typeof(CustomConverterForClass))] // Invalid
256+
public struct StructWithBadCustomConverter
257+
{
258+
public int MyInt { get; set; }
259+
}
154260
}

0 commit comments

Comments
 (0)