Skip to content
46 changes: 42 additions & 4 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ private sealed partial class Emitter
private const string ReferenceHandlerTypeRef = "global::System.Text.Json.Serialization.ReferenceHandler";
private const string EmptyTypeArray = "global::System.Array.Empty<global::System.Type>()";

private const string ByteArrayValueWriterMethodName = "WriteByteArrayValue";

/// <summary>
/// Contains an index from TypeRef to TypeGenerationSpec for the current ContextGenerationSpec.
/// </summary>
Expand All @@ -84,6 +86,12 @@ private sealed partial class Emitter
/// </summary>
private bool _emitValueTypeSetterDelegate;

/// <summary>
/// Indicates that the fast-path serializer writes a <see cref="byte"/> array value,
/// requiring the on-demand byte[] writer helper to be emitted on the context.
/// </summary>
private bool _emitByteArrayValueHelper;

/// <summary>
/// The SourceText emit implementation filled by the individual Roslyn versions.
/// </summary>
Expand Down Expand Up @@ -112,7 +120,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec)
string contextName = contextGenerationSpec.ContextType.Name;

// Add root context implementation.
AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec, _emitGetConverterForNullablePropertyMethod, _emitValueTypeSetterDelegate));
AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec, _emitGetConverterForNullablePropertyMethod, _emitValueTypeSetterDelegate, _emitByteArrayValueHelper));

// Add GetJsonTypeInfo override implementation.
AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(contextGenerationSpec));
Expand All @@ -122,6 +130,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec)

_emitGetConverterForNullablePropertyMethod = false;
_emitValueTypeSetterDelegate = false;
_emitByteArrayValueHelper = false;
_propertyNames.Clear();
_typeIndex.Clear();
}
Expand Down Expand Up @@ -1775,14 +1784,19 @@ private static void GenerateFastPathFuncHeader(SourceWriter writer, TypeGenerati
}
}

private static void GenerateSerializeValueStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string valueExpr)
private void GenerateSerializeValueStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string valueExpr)
{
if (GetPrimitiveWriterMethod(typeSpec) is string primitiveWriterMethod)
{
if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char)
{
writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr}.ToString());");
}
else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray)
{
writer.WriteLine($"{ByteArrayValueWriterMethodName}(writer, {valueExpr});");
_emitByteArrayValueHelper = true;
}
else
{
writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr});");
Expand All @@ -1801,14 +1815,20 @@ private static void GenerateSerializeValueStatement(SourceWriter writer, TypeGen
}
}

private static void GenerateSerializePropertyStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string propertyNameExpr, string valueExpr)
private void GenerateSerializePropertyStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string propertyNameExpr, string valueExpr)
{
if (GetPrimitiveWriterMethod(typeSpec) is string primitiveWriterMethod)
{
if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char)
{
writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr}.ToString());");
}
else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray)
{
writer.WriteLine($"writer.WritePropertyName({propertyNameExpr});");
writer.WriteLine($"{ByteArrayValueWriterMethodName}(writer, {valueExpr});");
_emitByteArrayValueHelper = true;
}
else
{
writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr});");
Expand Down Expand Up @@ -1892,7 +1912,7 @@ private static void GenerateTypeInfoFactoryFooter(SourceWriter writer)
""");
}

private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod, bool emitValueTypeSetterDelegate)
private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod, bool emitValueTypeSetterDelegate, bool emitByteArrayValueHelper)
{
string contextTypeRef = contextSpec.ContextType.FullyQualifiedName;
string contextTypeName = contextSpec.ContextType.Name;
Expand Down Expand Up @@ -1946,6 +1966,24 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec

writer.WriteLine();

if (emitByteArrayValueHelper)
{
writer.WriteLine($$"""
private static void {{ByteArrayValueWriterMethodName}}({{Utf8JsonWriterTypeRef}} writer, byte[]? value)
{
if (value is null)
{
writer.WriteNullValue();
}
else
{
writer.WriteBase64StringValue(value);
}
}
""");
writer.WriteLine();
}

GenerateConverterHelpers(writer, emitGetConverterForNullablePropertyMethod);

return CompleteSourceFileAndReturnText(writer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1164,5 +1164,42 @@ public static void SupportsAsymmetricNestedConverterWithManyParams()
internal partial class NestedGenericConverterContext : JsonSerializerContext
{
}

public class ClassWithMultipleByteArrayProperties
{
public byte[]? Data1 { get; set; }
public byte[]? Data2 { get; set; }
public byte[]? Data3 { get; set; }
}

[JsonSerializable(typeof(ClassWithMultipleByteArrayProperties))]
internal partial class MultipleByteArrayContext : JsonSerializerContext
{
}

[Theory]
[InlineData(new byte[] { 1, 2, 3 }, new byte[] { 4, 5, 6 }, null)]
[InlineData(null, new byte[] { 4, 5, 6 }, new byte[] { 7, 8, 9 })]
[InlineData(null, null, null)]
public static void ClassWithMultipleByteArrayProperties_Roundtrip(byte[]? data1, byte[]? data2, byte[]? data3)
{
var value = new ClassWithMultipleByteArrayProperties
{
Data1 = data1,
Data2 = data2,
Data3 = data3,
};

string json = JsonSerializer.Serialize(value, MultipleByteArrayContext.Default.ClassWithMultipleByteArrayProperties);

if (data1 is null) Assert.Contains("\"Data1\":null", json);
if (data2 is null) Assert.Contains("\"Data2\":null", json);
if (data3 is null) Assert.Contains("\"Data3\":null", json);

ClassWithMultipleByteArrayProperties deserialized = JsonSerializer.Deserialize(json, MultipleByteArrayContext.Default.ClassWithMultipleByteArrayProperties);
Assert.Equal(value.Data1, deserialized.Data1);
Assert.Equal(value.Data2, deserialized.Data2);
Assert.Equal(value.Data3, deserialized.Data3);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ public virtual void ClassWithNullableProperties_Roundtrip()
{
Uri = new Uri("http://contoso.com"),
Array = new int[] { 42 },
ByteArray = new byte[] { 1, 2, 3 },
Poco = new ClassWithNullableProperties.MyPoco(),

NullableUri = new Uri("http://contoso.com"),
Expand All @@ -885,10 +886,17 @@ public virtual void ClassWithNullableProperties_Roundtrip()
void RunTest(ClassWithNullableProperties expected)
{
string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithNullableProperties);

if (expected.ByteArray is null)
{
Assert.Contains("\"ByteArray\":null", json);
}

ClassWithNullableProperties actual = JsonSerializer.Deserialize(json, DefaultContext.ClassWithNullableProperties);

Assert.Equal(expected.Uri, actual.Uri);
Assert.Equal(expected.Array, actual.Array);
Assert.Equal(expected.ByteArray, actual.ByteArray);
Assert.Equal(expected.Poco, actual.Poco);

Assert.Equal(expected.NullableUri, actual.NullableUri);
Expand Down Expand Up @@ -943,6 +951,7 @@ public class ClassWithNullableProperties
{
public Uri? Uri { get; set; }
public int[]? Array { get; set; }
public byte[]? ByteArray { get; set; }
public MyPoco? Poco { get; set; }

public Uri? NullableUri { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ public override void ClassWithNullableProperties_Roundtrip()
{
Uri = new Uri("http://contoso.com"),
Array = new int[] { 42 },
ByteArray = new byte[] { 1, 2, 3 },
Comment thread
eiriktsarpalis marked this conversation as resolved.
Poco = new ClassWithNullableProperties.MyPoco(),

NullableUri = new Uri("http://contoso.com"),
Expand All @@ -467,10 +468,17 @@ public override void ClassWithNullableProperties_Roundtrip()
void RunTest(ClassWithNullableProperties expected)
{
string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithNullableProperties);

if (expected.ByteArray is null)
{
Assert.Contains("\"ByteArray\":null", json);
}

ClassWithNullableProperties actual = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).ClassWithNullableProperties);

Assert.Equal(expected.Uri, actual.Uri);
Assert.Equal(expected.Array, actual.Array);
Assert.Equal(expected.ByteArray, actual.ByteArray);
Assert.Equal(expected.Poco, actual.Poco);

Assert.Equal(expected.NullableUri, actual.NullableUri);
Expand Down
Loading