Skip to content

Commit 5a0f438

Browse files
authored
Include properties on records for (de)serialization in source-gen (#62668)
1 parent 7fc4b6a commit 5a0f438

File tree

3 files changed

+251
-10
lines changed

3 files changed

+251
-10
lines changed

src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,6 @@ public override PropertyInfo[] GetProperties(BindingFlags bindingAttr)
396396
{
397397
if (item is IPropertySymbol propertySymbol)
398398
{
399-
// Skip auto-generated properties on records.
400-
if (_typeSymbol.IsRecord && propertySymbol.DeclaringSyntaxReferences.Length == 0)
401-
{
402-
continue;
403-
}
404-
405399
// Skip if:
406400
if (
407401
// we want a static property and this is not static

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,59 @@ public partial class MyJsonContext : JsonSerializerContext
336336
return CreateCompilation(source);
337337
}
338338

339+
public static Compilation CreateReferencedLibRecordCompilation()
340+
{
341+
string source = @"
342+
using System.Text.Json.Serialization;
343+
344+
namespace ReferencedAssembly
345+
{
346+
public record LibRecord(int Id)
347+
{
348+
public string Address1 { get; set; }
349+
public string Address2 { get; set; }
350+
public string City { get; set; }
351+
public string State { get; set; }
352+
public string PostalCode { get; set; }
353+
public string Name { get; set; }
354+
[JsonInclude]
355+
public string PhoneNumber;
356+
[JsonInclude]
357+
public string Country;
358+
}
359+
}
360+
";
361+
362+
return CreateCompilation(source);
363+
}
364+
365+
public static Compilation CreateReferencedSimpleLibRecordCompilation()
366+
{
367+
string source = @"
368+
using System.Text.Json.Serialization;
369+
370+
namespace ReferencedAssembly
371+
{
372+
public record LibRecord
373+
{
374+
public int Id { get; set; }
375+
public string Address1 { get; set; }
376+
public string Address2 { get; set; }
377+
public string City { get; set; }
378+
public string State { get; set; }
379+
public string PostalCode { get; set; }
380+
public string Name { get; set; }
381+
[JsonInclude]
382+
public string PhoneNumber;
383+
[JsonInclude]
384+
public string Country;
385+
}
386+
}
387+
";
388+
389+
return CreateCompilation(source);
390+
}
391+
339392
internal static void CheckDiagnosticMessages(
340393
DiagnosticSeverity level,
341394
ImmutableArray<Diagnostic> diagnostics,

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs

Lines changed: 198 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,22 +449,216 @@ public void UsePrivates()
449449
CheckFieldsPropertiesMethods(myType, expectedFieldNames, expectedPropertyNames, expectedMethodNames);
450450
}
451451

452+
[Fact]
453+
public void Record()
454+
{
455+
// Compile the referenced assembly first.
456+
Compilation referencedCompilation = CompilationHelper.CreateReferencedLibRecordCompilation();
457+
458+
// Emit the image of the referenced assembly.
459+
byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation);
460+
461+
string source = @"
462+
using System.Text.Json.Serialization;
463+
464+
namespace HelloWorld
465+
{
466+
[JsonSerializable(typeof(AppRecord))]
467+
internal partial class JsonContext : JsonSerializerContext
468+
{
469+
}
470+
471+
public record AppRecord(int Id)
472+
{
473+
public string Address1 { get; set; }
474+
public string Address2 { get; set; }
475+
public string City { get; set; }
476+
public string State { get; set; }
477+
public string PostalCode { get; set; }
478+
public string Name { get; set; }
479+
[JsonInclude]
480+
public string PhoneNumber;
481+
[JsonInclude]
482+
public string Country;
483+
}
484+
}";
485+
486+
MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };
487+
488+
Compilation compilation = CompilationHelper.CreateCompilation(source);
489+
490+
JsonSourceGenerator generator = new JsonSourceGenerator();
491+
492+
Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out ImmutableArray<Diagnostic> generatorDiags, generator);
493+
494+
// Make sure compilation was successful.
495+
CheckCompilationDiagnosticsErrors(generatorDiags);
496+
CheckCompilationDiagnosticsErrors(newCompilation.GetDiagnostics());
497+
498+
Dictionary<string, Type> types = generator.GetSerializableTypes();
499+
500+
// Check base functionality of found types.
501+
Assert.Equal(1, types.Count);
502+
Type recordType = types["HelloWorld.AppRecord"];
503+
Assert.Equal("HelloWorld.AppRecord", recordType.FullName);
504+
505+
// Check for received fields, properties and methods for NotMyType.
506+
string[] expectedFieldsNames = { "Country", "PhoneNumber" };
507+
string[] expectedPropertyNames = { "Address1", "Address2", "City", "Id", "Name", "PostalCode", "State" };
508+
CheckFieldsPropertiesMethods(recordType, expectedFieldsNames, expectedPropertyNames);
509+
510+
Assert.Equal(1, recordType.GetConstructors().Length);
511+
}
512+
513+
[Fact]
514+
public void RecordInExternalAssembly()
515+
{
516+
// Compile the referenced assembly first.
517+
Compilation referencedCompilation = CompilationHelper.CreateReferencedLibRecordCompilation();
518+
519+
// Emit the image of the referenced assembly.
520+
byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation);
521+
522+
string source = @"
523+
using System.Text.Json.Serialization;
524+
using ReferencedAssembly;
525+
526+
namespace HelloWorld
527+
{
528+
[JsonSerializable(typeof(LibRecord))]
529+
internal partial class JsonContext : JsonSerializerContext
530+
{
531+
}
532+
}";
533+
534+
MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };
535+
536+
Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
537+
538+
JsonSourceGenerator generator = new JsonSourceGenerator();
539+
540+
Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out ImmutableArray<Diagnostic> generatorDiags, generator);
541+
542+
// Make sure compilation was successful.
543+
CheckCompilationDiagnosticsErrors(generatorDiags);
544+
CheckCompilationDiagnosticsErrors(newCompilation.GetDiagnostics());
545+
546+
Dictionary<string, Type> types = generator.GetSerializableTypes();
547+
548+
Assert.Equal(1, types.Count);
549+
Type recordType = types["ReferencedAssembly.LibRecord"];
550+
Assert.Equal("ReferencedAssembly.LibRecord", recordType.FullName);
551+
552+
string[] expectedFieldsNames = { "Country", "PhoneNumber" };
553+
string[] expectedPropertyNames = { "Address1", "Address2", "City", "Id", "Name", "PostalCode", "State" };
554+
CheckFieldsPropertiesMethods(recordType, expectedFieldsNames, expectedPropertyNames);
555+
556+
Assert.Equal(1, recordType.GetConstructors().Length);
557+
}
558+
559+
[Fact]
560+
public void RecordDerivedFromRecordInExternalAssembly()
561+
{
562+
// Compile the referenced assembly first.
563+
Compilation referencedCompilation = CompilationHelper.CreateReferencedSimpleLibRecordCompilation();
564+
565+
// Emit the image of the referenced assembly.
566+
byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation);
567+
568+
string source = @"
569+
using System.Text.Json.Serialization;
570+
using ReferencedAssembly;
571+
572+
namespace HelloWorld
573+
{
574+
[JsonSerializable(typeof(AppRecord))]
575+
internal partial class JsonContext : JsonSerializerContext
576+
{
577+
}
578+
579+
internal record AppRecord : LibRecord
580+
{
581+
public string ExtraData { get; set; }
582+
}
583+
}";
584+
585+
MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };
586+
587+
Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
588+
589+
JsonSourceGenerator generator = new JsonSourceGenerator();
590+
591+
Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out ImmutableArray<Diagnostic> generatorDiags, generator);
592+
593+
// Make sure compilation was successful.
594+
CheckCompilationDiagnosticsErrors(generatorDiags);
595+
CheckCompilationDiagnosticsErrors(newCompilation.GetDiagnostics());
596+
597+
Dictionary<string, Type> types = generator.GetSerializableTypes();
598+
599+
Assert.Equal(1, types.Count);
600+
Type recordType = types["HelloWorld.AppRecord"];
601+
Assert.Equal("HelloWorld.AppRecord", recordType.FullName);
602+
603+
string[] expectedFieldsNames = { "Country", "PhoneNumber" };
604+
string[] expectedPropertyNames = { "Address1", "Address2", "City", "ExtraData", "Id", "Name", "PostalCode", "State" };
605+
CheckFieldsPropertiesMethods(recordType, expectedFieldsNames, expectedPropertyNames, inspectBaseTypes: true);
606+
607+
Assert.Equal(1, recordType.GetConstructors().Length);
608+
}
609+
452610
private void CheckCompilationDiagnosticsErrors(ImmutableArray<Diagnostic> diagnostics)
453611
{
454612
Assert.Empty(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error));
455613
}
456614

457-
private void CheckFieldsPropertiesMethods(Type type, string[] expectedFields, string[] expectedProperties, string[] expectedMethods)
615+
private void CheckFieldsPropertiesMethods(
616+
Type type,
617+
string[] expectedFields,
618+
string[] expectedProperties,
619+
string[] expectedMethods = null,
620+
bool inspectBaseTypes = false)
458621
{
459622
BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
460623

461-
string[] receivedFields = type.GetFields(bindingFlags).Select(field => field.Name).OrderBy(s => s).ToArray();
462-
string[] receivedProperties = type.GetProperties(bindingFlags).Select(property => property.Name).OrderBy(s => s).ToArray();
624+
string[] receivedFields;
625+
string[] receivedProperties;
626+
627+
if (!inspectBaseTypes)
628+
{
629+
receivedFields = type.GetFields(bindingFlags).Select(field => field.Name).OrderBy(s => s).ToArray();
630+
receivedProperties = type.GetProperties(bindingFlags).Select(property => property.Name).OrderBy(s => s).ToArray();
631+
}
632+
else
633+
{
634+
List<string> fields = new List<string>();
635+
List<string> props = new List<string>();
636+
637+
Type currentType = type;
638+
while (currentType != null)
639+
{
640+
fields.AddRange(currentType.GetFields(bindingFlags).Select(property => property.Name).OrderBy(s => s).ToArray());
641+
props.AddRange(currentType.GetProperties(bindingFlags).Select(property => property.Name).OrderBy(s => s).ToArray());
642+
currentType = currentType.BaseType;
643+
}
644+
645+
receivedFields = fields.ToArray();
646+
receivedProperties = props.ToArray();
647+
}
648+
463649
string[] receivedMethods = type.GetMethods().Select(method => method.Name).OrderBy(s => s).ToArray();
464650

651+
Array.Sort(receivedFields);
652+
Array.Sort(receivedProperties);
653+
Array.Sort(receivedMethods);
654+
465655
Assert.Equal(expectedFields, receivedFields);
466656
Assert.Equal(expectedProperties, receivedProperties);
467-
Assert.Equal(expectedMethods, receivedMethods);
657+
658+
if (expectedMethods != null)
659+
{
660+
Assert.Equal(expectedMethods, receivedMethods);
661+
}
468662
}
469663

470664
// TODO: add test guarding against (de)serializing static classes.

0 commit comments

Comments
 (0)