Skip to content

Commit a035411

Browse files
committed
Add source gen support + misc feedback
1 parent c4bed8b commit a035411

16 files changed

+106
-10
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json.SourceGeneration
5+
{
6+
internal static class JsonConstants
7+
{
8+
public const string IJsonOnSerializedFullName = "System.Text.Json.Serialization.IJsonOnSerialized";
9+
public const string IJsonOnSerializingFullName = "System.Text.Json.Serialization.IJsonOnSerializing";
10+
}
11+
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,13 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)
500500

501501
if (typeMetadata.GenerateSerializationLogic)
502502
{
503-
serializeFuncSource = GenerateFastPathFuncForObject(typeCompilableName, serializeMethodName, typeMetadata.CanBeNull, properties);
503+
serializeFuncSource = GenerateFastPathFuncForObject(
504+
typeCompilableName,
505+
serializeMethodName,
506+
typeMetadata.CanBeNull,
507+
typeMetadata.ImplementsIJsonOnSerialized,
508+
typeMetadata.ImplementsIJsonOnSerializing,
509+
properties);
504510
serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}";
505511
}
506512
else
@@ -635,6 +641,8 @@ private string GenerateFastPathFuncForObject(
635641
string typeInfoTypeRef,
636642
string serializeMethodName,
637643
bool canBeNull,
644+
bool implementsIJsonOnSerialized,
645+
bool implementsIJsonOnSerializing,
638646
List<PropertyGenerationSpec>? properties)
639647
{
640648
JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions;
@@ -646,6 +654,12 @@ private string GenerateFastPathFuncForObject(
646654
StringBuilder sb = new();
647655

648656
// Begin method definition
657+
if (implementsIJsonOnSerializing)
658+
{
659+
sb.Append($@"(({JsonConstants.IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();");
660+
sb.Append($@"{Environment.NewLine} ");
661+
}
662+
649663
sb.Append($@"{WriterVarName}.WriteStartObject();");
650664

651665
if (properties != null)
@@ -733,6 +747,12 @@ private string GenerateFastPathFuncForObject(
733747
734748
{WriterVarName}.WriteEndObject();");
735749

750+
if (implementsIJsonOnSerialized)
751+
{
752+
sb.Append($@"{Environment.NewLine} ");
753+
sb.Append($@"(({JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();");
754+
};
755+
736756
return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull);
737757
}
738758

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
494494
bool foundDesignTimeCustomConverter = false;
495495
string? converterInstatiationLogic = null;
496496

497+
bool implementsIJsonOnSerialized = false;
498+
bool implementsIJsonOnSerializing = false;
499+
497500
IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(type);
498501
foreach (CustomAttributeData attributeData in attributeDataList)
499502
{
@@ -577,6 +580,11 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
577580
constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
578581
}
579582

583+
// GetInterface() is currently not implemented, so we use GetInterfaces().
584+
IEnumerable<string> interfaces = type.GetInterfaces().Select(interfaceType => interfaceType.FullName);
585+
implementsIJsonOnSerialized = interfaces.FirstOrDefault(interfaceName => interfaceName == JsonConstants.IJsonOnSerializedFullName) != null;
586+
implementsIJsonOnSerializing = interfaces.FirstOrDefault(interfaceName => interfaceName == JsonConstants.IJsonOnSerializingFullName) != null;
587+
580588
for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
581589
{
582590
const BindingFlags bindingFlags =
@@ -627,7 +635,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
627635
collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType, generationMode) : null,
628636
constructionStrategy,
629637
nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode) : null,
630-
converterInstatiationLogic);
638+
converterInstatiationLogic,
639+
implementsIJsonOnSerialized,
640+
implementsIJsonOnSerializing);
631641

632642
return typeMetadata;
633643
}

src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0</TargetFrameworks>
44
<CLSCompliant>false</CLSCompliant>
@@ -35,6 +35,7 @@
3535
<Compile Include="..\Common\JsonSourceGenerationOptionsAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationOptionsAttribute.cs" />
3636
<Compile Include="ClassType.cs" />
3737
<Compile Include="CollectionType.cs" />
38+
<Compile Include="JsonConstants.cs" />
3839
<Compile Include="JsonSourceGenerator.cs" />
3940
<Compile Include="JsonSourceGenerator.Emitter.cs" />
4041
<Compile Include="JsonSourceGenerator.Parser.cs" />

src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ internal class TypeGenerationSpec
3333

3434
public ClassType ClassType { get; private set; }
3535

36+
public bool ImplementsIJsonOnSerialized { get; private set; }
37+
public bool ImplementsIJsonOnSerializing { get; private set; }
38+
3639
public bool IsValueType { get; private set; }
3740

3841
public bool CanBeNull { get; private set; }
@@ -67,7 +70,9 @@ public void Initialize(
6770
TypeGenerationSpec? collectionValueTypeMetadata,
6871
ObjectConstructionStrategy constructionStrategy,
6972
TypeGenerationSpec? nullableUnderlyingTypeMetadata,
70-
string? converterInstantiationLogic)
73+
string? converterInstantiationLogic,
74+
bool implementsIJsonOnSerialized,
75+
bool implementsIJsonOnSerializing)
7176
{
7277
GenerationMode = generationMode;
7378
TypeRef = $"global::{typeRef}";
@@ -84,6 +89,8 @@ public void Initialize(
8489
ConstructionStrategy = constructionStrategy;
8590
NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata;
8691
ConverterInstantiationLogic = converterInstantiationLogic;
92+
ImplementsIJsonOnSerialized = implementsIJsonOnSerialized;
93+
ImplementsIJsonOnSerializing = implementsIJsonOnSerializing;
8794
}
8895

8996
private bool FastPathIsSupported()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization
88
/// </summary>
99
/// <remarks>
1010
/// This behavior is only supported on types representing JSON objects.
11-
/// Types that have a custom converter or represent collections or values do not support this behavior.
11+
/// Types that have a custom converter or represent either collections or primitive values do not support this behavior.
1212
/// </remarks>
1313
public interface IJsonOnDeserialized
1414
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization
88
/// </summary>
99
/// <remarks>
1010
/// This behavior is only supported on types representing JSON objects.
11-
/// Types that have a custom converter or represent collections or values do not support this behavior.
11+
/// Types that have a custom converter or represent either collections or primitive values do not support this behavior.
1212
/// </remarks>
1313
public interface IJsonOnDeserializing
1414
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization
88
/// </summary>
99
/// <remarks>
1010
/// This behavior is only supported on types representing JSON objects.
11-
/// Types that have a custom converter or represent collections or values do not support this behavior.
11+
/// Types that have a custom converter or represent either collections or primitive values do not support this behavior.
1212
/// </remarks>
1313
public interface IJsonOnSerialized
1414
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization
88
/// </summary>
99
/// <remarks>
1010
/// This behavior is only supported on types representing JSON objects.
11-
/// Types that have a custom converter or represent collections or values do not support this behavior.
11+
/// Types that have a custom converter or represent either collections or primitive values do not support this behavior.
1212
/// </remarks>
1313
public interface IJsonOnSerializing
1414
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public interface ITestContext
1919
public JsonTypeInfo<HighLowTemps> HighLowTemps { get; }
2020
public JsonTypeInfo<MyType> MyType { get; }
2121
public JsonTypeInfo<MyType2> MyType2 { get; }
22+
public JsonTypeInfo<MyTypeWithCallbacks> MyTypeWithCallbacks { get; }
2223
public JsonTypeInfo<MyIntermediateType> MyIntermediateType { get; }
2324
public JsonTypeInfo<HighLowTempsImmutable> HighLowTempsImmutable { get; }
2425
public JsonTypeInfo<RealWorldContextTests.MyNestedClass> MyNestedClass { get; }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace System.Text.Json.SourceGeneration.Tests
1717
[JsonSerializable(typeof(HighLowTemps))]
1818
[JsonSerializable(typeof(MyType))]
1919
[JsonSerializable(typeof(MyType2))]
20+
[JsonSerializable(typeof(MyTypeWithCallbacks))]
2021
[JsonSerializable(typeof(MyIntermediateType))]
2122
[JsonSerializable(typeof(HighLowTempsImmutable))]
2223
[JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))]
@@ -45,6 +46,7 @@ public override void EnsureFastPathGeneratedAsExpected()
4546
Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTemps.Serialize);
4647
Assert.NotNull(MetadataAndSerializationContext.Default.MyType.Serialize);
4748
Assert.NotNull(MetadataAndSerializationContext.Default.MyType2.Serialize);
49+
Assert.NotNull(MetadataAndSerializationContext.Default.MyTypeWithCallbacks.Serialize);
4850
Assert.NotNull(MetadataAndSerializationContext.Default.MyIntermediateType.Serialize);
4951
Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsImmutable.Serialize);
5052
Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace System.Text.Json.SourceGeneration.Tests
1616
[JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Metadata)]
1717
[JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Metadata)]
1818
[JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata)]
19+
[JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata)]
1920
[JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata)]
2021
[JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)]
2122
[JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)]
@@ -93,6 +94,7 @@ public override void EnsureFastPathGeneratedAsExpected()
9394
Assert.Null(MetadataContext.Default.HighLowTemps.Serialize);
9495
Assert.Null(MetadataContext.Default.MyType.Serialize);
9596
Assert.Null(MetadataContext.Default.MyType2.Serialize);
97+
Assert.Null(MetadataContext.Default.MyTypeWithCallbacks.Serialize);
9698
Assert.Null(MetadataContext.Default.MyIntermediateType.Serialize);
9799
Assert.Null(MetadataContext.Default.HighLowTempsImmutable.Serialize);
98100
Assert.Null(MetadataContext.Default.MyNestedClass.Serialize);

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace System.Text.Json.SourceGeneration.Tests
1616
[JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)]
1717
[JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Default)]
1818
[JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
19+
[JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
1920
[JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
2021
[JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)]
2122
[JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)]
@@ -43,6 +44,7 @@ public override void EnsureFastPathGeneratedAsExpected()
4344
Assert.NotNull(MixedModeContext.Default.HighLowTemps.Serialize);
4445
Assert.NotNull(MixedModeContext.Default.MyType.Serialize);
4546
Assert.NotNull(MixedModeContext.Default.MyType2.Serialize);
47+
Assert.NotNull(MixedModeContext.Default.MyTypeWithCallbacks.Serialize);
4648
Assert.NotNull(MixedModeContext.Default.MyIntermediateType.Serialize);
4749
Assert.Null(MixedModeContext.Default.HighLowTempsImmutable.Serialize);
4850
Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize);
@@ -165,5 +167,24 @@ public override void SerializeObjectArray_WithCustomOptions()
165167
VerifyIndexViewModel(index, JsonSerializer.Deserialize(indexAsJsonElement.GetRawText(), metadataContext.IndexViewModel));
166168
VerifyCampaignSummaryViewModel(campaignSummary, JsonSerializer.Deserialize(campaignSummeryAsJsonElement.GetRawText(), metadataContext.CampaignSummaryViewModel));
167169
}
170+
171+
[Fact]
172+
public void OnSerializeCallbacks_WithCustomOptions()
173+
{
174+
MyTypeWithCallbacks obj = new();
175+
Assert.Null(obj.MyProperty);
176+
177+
ITestContext context = SerializationContextWithCamelCase.Default;
178+
Assert.Same(JsonNamingPolicy.CamelCase, ((JsonSerializerContext)context).Options.PropertyNamingPolicy);
179+
180+
string json = JsonSerializer.Serialize(obj, context.MyTypeWithCallbacks);
181+
Assert.Equal("{\"myProperty\":\"Before\"}", json);
182+
Assert.Equal("After", obj.MyProperty);
183+
184+
context = new MetadataContext(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
185+
json = JsonSerializer.Serialize(obj, context.MyTypeWithCallbacks);
186+
Assert.Equal("{\"myProperty\":\"Before\"}", json);
187+
Assert.Equal("After", obj.MyProperty);
188+
}
168189
}
169190
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex
3838
[JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)]
3939
[JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)]
4040
[JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)]
41+
[JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)]
4142
[JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)]
4243
[JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)]
4344
[JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)]
@@ -60,6 +61,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer
6061
[JsonSerializable(typeof(HighLowTemps), GenerationMode = JsonSourceGenerationMode.Serialization)]
6162
[JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)]
6263
[JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)]
64+
[JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)]
6365
[JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)]
6466
[JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)]
6567
[JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)]
@@ -93,6 +95,7 @@ public override void EnsureFastPathGeneratedAsExpected()
9395
Assert.NotNull(SerializationContext.Default.HighLowTemps.Serialize);
9496
Assert.NotNull(SerializationContext.Default.MyType.Serialize);
9597
Assert.NotNull(SerializationContext.Default.MyType2.Serialize);
98+
Assert.NotNull(SerializationContext.Default.MyTypeWithCallbacks.Serialize);
9699
Assert.NotNull(SerializationContext.Default.MyIntermediateType.Serialize);
97100
Assert.NotNull(SerializationContext.Default.HighLowTempsImmutable.Serialize);
98101
Assert.NotNull(SerializationContext.Default.MyNestedClass.Serialize);
@@ -307,6 +310,17 @@ public override void ParameterizedConstructor()
307310

308311
JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsImmutable), typeof(HighLowTempsImmutable));
309312
}
313+
314+
[Fact]
315+
public void OnSerializeCallbacks()
316+
{
317+
MyTypeWithCallbacks obj = new();
318+
Assert.Null(obj.MyProperty);
319+
320+
string json = JsonSerializer.Serialize(obj, DefaultContext.MyTypeWithCallbacks);
321+
Assert.Equal("{\"MyProperty\":\"Before\"}", json);
322+
Assert.Equal("After", obj.MyProperty);
323+
}
310324
}
311325

312326
public sealed class SerializationWithPerTypeAttributeContextTests : SerializationContextTests

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Text.Json.Serialization;
56

67
namespace System.Text.Json.SourceGeneration.Tests.RepeatedTypes
78
{
@@ -108,6 +109,14 @@ public class MyIntermediateType
108109
public MyType Type = new();
109110
}
110111

112+
public class MyTypeWithCallbacks : IJsonOnSerializing, IJsonOnSerialized
113+
{
114+
public string MyProperty { get; set; }
115+
116+
public void OnSerializing() => MyProperty = "Before";
117+
void IJsonOnSerialized.OnSerialized() => MyProperty = "After";
118+
}
119+
111120
public class JsonMessage
112121
{
113122
public string Message { get; set; }

src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OnSerializeTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ private class MyClassWithSmallConstructor :
149149
{
150150
public int MyInt { get; set; }
151151

152-
[JsonConstructor]
153152
public MyClassWithSmallConstructor(int myInt)
154153
{
155154
MyInt = myInt;
@@ -229,7 +228,6 @@ private class MyClassWithLargeConstructor :
229228
public int MyInt4 { get; set; }
230229
public int MyInt5 { get; set; }
231230

232-
[JsonConstructor]
233231
public MyClassWithLargeConstructor(int myInt1, int myInt2, int myInt3, int myInt4, int myInt5)
234232
{
235233
MyInt1 = myInt1;

0 commit comments

Comments
 (0)