Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Src-Gen serialization for nullable structs (#59719)
  • Loading branch information
Sychev Vadim committed Oct 13, 2021
commit 96102333b20331437ca037441803c758fd502b14
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