From 99eedd0ddd0d84c009c2f9572ddde3c849b7080c Mon Sep 17 00:00:00 2001 From: Artur Ispiriants Date: Thu, 25 Jun 2026 03:18:43 +0200 Subject: [PATCH 1/2] [JSON] Fix source generator serializing null byte[] as empty string The source generator fast path emitted `writer.WriteBase64String(value)` directly for byte[] members. Since byte[] implicitly converts to an empty ReadOnlySpan when null, a null byte[] was serialized as an empty Base64 string ("") instead of null, diverging from the reflection-based ByteArrayConverter which writes null. Emit an explicit null check in the fast-path serialization helpers for JsonPrimitiveTypeKind.ByteArray, mirroring ByteArrayConverter.Write, so that null byte[] members serialize as null for both object properties and collection elements. Co-Authored-By: Claude Opus 4.8 --- .../gen/JsonSourceGenerator.Emitter.cs | 30 +++++++++++++++++++ .../RealWorldContextTests.cs | 3 ++ .../SerializationContextTests.cs | 2 ++ 3 files changed, 35 insertions(+) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index cd24cda84f0bd3..986d0a6883cf82 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1783,6 +1783,21 @@ private static void GenerateSerializeValueStatement(SourceWriter writer, TypeGen { writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr}.ToString());"); } + else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) + { + // byte[] is a nullable reference type; emit an explicit null check so that null values + // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. + writer.WriteLine($$""" + if ({{valueExpr}} is null) + { + writer.WriteNullValue(); + } + else + { + writer.{{primitiveWriterMethod}}Value({{valueExpr}}); + } + """); + } else { writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr});"); @@ -1809,6 +1824,21 @@ private static void GenerateSerializePropertyStatement(SourceWriter writer, Type { writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr}.ToString());"); } + else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) + { + // byte[] is a nullable reference type; emit an explicit null check so that null values + // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. + writer.WriteLine($$""" + if ({{valueExpr}} is null) + { + writer.WriteNull({{propertyNameExpr}}); + } + else + { + writer.{{primitiveWriterMethod}}({{propertyNameExpr}}, {{valueExpr}}); + } + """); + } else { writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr});"); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index 3cfcdcdc03a7e9..f24387da387891 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -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"), @@ -889,6 +890,7 @@ void RunTest(ClassWithNullableProperties expected) 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); @@ -943,6 +945,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; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index b557871e76d460..48eb0e377e0276 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -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 }, Poco = new ClassWithNullableProperties.MyPoco(), NullableUri = new Uri("http://contoso.com"), @@ -471,6 +472,7 @@ void RunTest(ClassWithNullableProperties expected) 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); From 895c9671c1a0bc3de14002d112c47fa89251b032 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 25 Jun 2026 09:38:40 +0300 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../gen/JsonSourceGenerator.Emitter.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 986d0a6883cf82..abf92ca701d8a6 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1783,21 +1783,21 @@ private static void GenerateSerializeValueStatement(SourceWriter writer, TypeGen { writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr}.ToString());"); } - else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) - { - // byte[] is a nullable reference type; emit an explicit null check so that null values - // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. - writer.WriteLine($$""" - if ({{valueExpr}} is null) - { - writer.WriteNullValue(); - } - else - { - writer.{{primitiveWriterMethod}}Value({{valueExpr}}); - } - """); - } +else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) +{ + // byte[] is a nullable reference type; emit an explicit null check so that null values + // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. + writer.WriteLine($$""" + if ({{valueExpr}} is byte[] __byteArray) + { + writer.{{primitiveWriterMethod}}Value(__byteArray); + } + else + { + writer.WriteNullValue(); + } + """); +} else { writer.WriteLine($"writer.{primitiveWriterMethod}Value({valueExpr});"); @@ -1824,21 +1824,21 @@ private static void GenerateSerializePropertyStatement(SourceWriter writer, Type { writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr}.ToString());"); } - else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) - { - // byte[] is a nullable reference type; emit an explicit null check so that null values - // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. - writer.WriteLine($$""" - if ({{valueExpr}} is null) - { - writer.WriteNull({{propertyNameExpr}}); - } - else - { - writer.{{primitiveWriterMethod}}({{propertyNameExpr}}, {{valueExpr}}); - } - """); - } +else if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.ByteArray) +{ + // byte[] is a nullable reference type; emit an explicit null check so that null values + // serialize as 'null' rather than an empty Base64 string, matching reflection behavior. + writer.WriteLine($$""" + if ({{valueExpr}} is byte[] __byteArray) + { + writer.{{primitiveWriterMethod}}({{propertyNameExpr}}, __byteArray); + } + else + { + writer.WriteNull({{propertyNameExpr}}); + } + """); +} else { writer.WriteLine($"writer.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr});");