Skip to content

Commit

Permalink
Src-Gen serialization for nullable structs (dotnet#59719)
Browse files Browse the repository at this point in the history
  • Loading branch information
SkiFoD committed Oct 13, 2021
1 parent 2e8615b commit 9610233
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,56 @@ private string GenerateForNullable(TypeGenerationSpec typeMetadata)

TypeGenerationSpec? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata;
Debug.Assert(underlyingTypeMetadata != null);

ObjectConstructionStrategy constructionStrategy = underlyingTypeMetadata.ConstructionStrategy;

string? propMetadataInitFuncSource = null;
string? ctorParamMetadataInitFuncSource = null;

string propInitMethodName = "null";
string ctorParamMetadataInitMethodName = "null";


if (typeMetadata.GenerateSerializationLogic)
{
string? serializeFuncSource = GenerateFastPathFuncForNullableStruct(typeMetadata);
string serializeMethodName = $"{typeFriendlyName}{SerializeHandlerPropName}";

const string ObjectInfoVarName = "objectInfo";

string genericArg = typeMetadata.TypeRef;

string creatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor
? $"static () => new {underlyingTypeMetadata.TypeRef}()"
: "null";

string parameterizedCreatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor
? GetParameterizedCtorInvocationFunc(typeMetadata.NullableUnderlyingTypeMetadata)
: "null";

if (typeMetadata.GenerateMetadata)
{
propInitMethodName = $"{underlyingTypeMetadata.TypeInfoPropertyName}{PropInitMethodNameSuffix}";

}

string objectInfoInitSource = $@"{JsonObjectInfoValuesTypeRef}<{genericArg}> {ObjectInfoVarName} = new {JsonObjectInfoValuesTypeRef}<{genericArg}>()
{{
{ObjectCreatorPropName} = {creatorInvocation},
ObjectWithParameterizedConstructorCreator = {parameterizedCreatorInvocation},
PropertyMetadataInitializer = {propInitMethodName},
ConstructorParameterMetadataInitializer = {ctorParamMetadataInitMethodName},
{NumberHandlingPropName} = {GetNumberHandlingAsStr(typeMetadata.NumberHandling)},
{SerializeHandlerPropName} = {serializeMethodName}
}};
_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeCompilableName}>({OptionsInstanceVariableName}, {ObjectInfoVarName});";

string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}";

return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource);
}

string underlyingTypeCompilableName = underlyingTypeMetadata.TypeRef;
string underlyingTypeFriendlyName = underlyingTypeMetadata.TypeInfoPropertyName;
string underlyingTypeInfoNamedArg = underlyingTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen
Expand Down Expand Up @@ -672,7 +722,7 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)

return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource);
}

private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec)
{
const string PropVarName = "properties";
Expand Down Expand Up @@ -824,6 +874,13 @@ private string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerati
return sb.ToString();
}

private string GenerateFastPathFuncForNullableStruct(TypeGenerationSpec typeGenSpec)
{
string serializeMethod = $"{typeGenSpec.NullableUnderlyingTypeMetadata.TypeInfoPropertyName}{SerializeHandlerPropName}(writer, value.Value);";

return GenerateFastPathFuncForType(typeGenSpec, serializeMethod, emitNullCheck: typeGenSpec.CanBeNull);
}

private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec)
{
JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions;
Expand Down
9 changes: 9 additions & 0 deletions src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ public bool TryFilterSerializableProps(

private bool FastPathIsSupported()
{

if (ClassType == ClassType.Nullable
&& (NullableUnderlyingTypeMetadata != null)
&& (NullableUnderlyingTypeMetadata.ClassType == ClassType.Object)
&& (NullableUnderlyingTypeMetadata.GenerateSerializationLogic))
{
return true;
}

if (ClassType == ClassType.Object)
{
if (ExtensionDataPropertyTypeSpec != null)
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ public static partial class JsonMetadataServices
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateIReadOnlyDictionaryInfo<TCollection, TKey, TValue>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.IReadOnlyDictionary<TKey, TValue> where TKey : notnull { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.ISet<TElement> { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.List<TElement> { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateObjectInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<T> objectInfo) where T : notnull { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateObjectInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<T> objectInfo) { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<T> propertyInfo) { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Queue<TElement> { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public static JsonPropertyInfo CreatePropertyInfo<T>(JsonSerializerOptions optio
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> or <paramref name="objectInfo"/> is null.</exception>
/// <returns>A <see cref="JsonTypeInfo{T}"/> instance representing the class or struct.</returns>
/// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, JsonObjectInfoValues<T> objectInfo) where T : notnull
public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, JsonObjectInfoValues<T> objectInfo)
=> new JsonTypeInfoInternal<T>(
options ?? throw new ArgumentNullException(nameof(options)),
objectInfo ?? throw new ArgumentNullException(nameof(objectInfo)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public interface ITestContext
public JsonTypeInfo<StructWithCustomConverterPropertyFactory> StructWithCustomConverterPropertyFactory { get; }
public JsonTypeInfo<ClassWithBadCustomConverter> ClassWithBadCustomConverter { get; }
public JsonTypeInfo<StructWithBadCustomConverter> StructWithBadCustomConverter { get; }
public JsonTypeInfo<MyStructWithProperties?> NullableMyStructWithProperties { get; }
public JsonTypeInfo<MyStructWithCtrProperties?> NullableMyStructWithCtrProperties { get; }
}

internal partial class JsonContext : JsonSerializerContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))]
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(MyStructWithProperties?))]
[JsonSerializable(typeof(MyStructWithCtrProperties?))]
internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Default;
Expand Down Expand Up @@ -83,8 +85,28 @@ public override void EnsureFastPathGeneratedAsExpected()
Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterProperty);
Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterPropertyFactory);
Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterPropertyFactory);
Assert.NotNull(MetadataAndSerializationContext.Default.MyStructWithProperties?.SerializeHandler);
Assert.NotNull(MetadataAndSerializationContext.Default.MyStructWithCtrProperties?.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter);
Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter);
}

[Fact]
public void Serialize_NullableStruct()
{
MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 };
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}

[Fact]
public void Serialize_NullableStructWithCtr()
{
MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2);
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Metadata)]
internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata;
Expand Down Expand Up @@ -81,6 +83,8 @@ public override void EnsureFastPathGeneratedAsExpected()
Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.SerializeHandler);
Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler);
Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler);
Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyStructWithProperties?.SerializeHandler);
Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyStructWithCtrProperties?.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.SerializeHandler);
}
Expand Down Expand Up @@ -119,7 +123,10 @@ public override void EnsureFastPathGeneratedAsExpected()
[JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))]
[JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))]
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(MyStructWithProperties?))]
[JsonSerializable(typeof(MyStructWithCtrProperties?))]

internal partial class MetadataContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata;
Expand Down Expand Up @@ -185,6 +192,8 @@ public override void EnsureFastPathGeneratedAsExpected()
Assert.Null(MetadataContext.Default.StructWithCustomConverterProperty.SerializeHandler);
Assert.Null(MetadataContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler);
Assert.Null(MetadataContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler);
Assert.Null(MetadataContext.Default.MyStructWithProperties?.SerializeHandler);
Assert.Null(MetadataContext.Default.MyStructWithCtrProperties?.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.ClassWithBadCustomConverter.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.StructWithBadCustomConverter.SerializeHandler);
}
Expand Down Expand Up @@ -225,5 +234,23 @@ public void EnsureHelperMethodGenerated_ImplicitPropertyFactory()
obj = JsonSerializer.Deserialize(Json, ContextWithImplicitStringEnum.Default.PocoWithEnum);
Assert.Equal(EnumWrittenAsString.A, obj.MyEnum);
}

[Fact]
public void Serialize_NullableStruct()
{
MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 };
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}

[Fact]
public void Serialize_NullableStructWithCtr()
{
MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2);
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
internal partial class MixedModeContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization;
Expand Down Expand Up @@ -83,6 +85,8 @@ public override void EnsureFastPathGeneratedAsExpected()
Assert.Null(MixedModeContext.Default.StructWithCustomConverterProperty.SerializeHandler);
Assert.Null(MixedModeContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler);
Assert.Null(MixedModeContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler);
Assert.NotNull(MixedModeContext.Default.MyStructWithProperties?.SerializeHandler);
Assert.NotNull(MixedModeContext.Default.MyStructWithCtrProperties?.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.ClassWithBadCustomConverter.SerializeHandler);
Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.StructWithBadCustomConverter.SerializeHandler);
}
Expand Down Expand Up @@ -219,5 +223,23 @@ public void OnSerializeCallbacks_WithCustomOptions()
Assert.Equal("{\"myProperty\":\"Before\"}", json);
Assert.Equal("After", obj.MyProperty);
}

[Fact]
public void Serialize_NullableStruct()
{
MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 };
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}

[Fact]
public void Serialize_NullableStructWithCtr()
{
MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2);
string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties);
string expected = "{\"B\":2,\"A\":1}";
Assert.Equal(expected, json);
}
}
}
Loading

1 comment on commit 9610233

@SkiFoD
Copy link
Owner Author

@SkiFoD SkiFoD commented on 9610233 Oct 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the final solution yet. I haven't implemented Fast-Path logic for nullable list wrappers yet.

Please sign in to comment.