diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs index 7abf6d32cddbf1..fe7e082ebcacfd 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs @@ -255,6 +255,10 @@ public KnownTypeSymbols(Compilation compilation) public INamedTypeSymbol? IJsonOnSerializedType => GetOrResolveType(JsonConstants.IJsonOnSerializedFullName, ref _IJsonOnSerializedType); private Option _IJsonOnSerializedType; + // Runtime feature detection types + public INamedTypeSymbol? UnsafeAccessorAttributeType => GetOrResolveType("System.Runtime.CompilerServices.UnsafeAccessorAttribute", ref _UnsafeAccessorAttributeType); + private Option _UnsafeAccessorAttributeType; + // Unsupported types public INamedTypeSymbol? DelegateType => _DelegateType ??= Compilation.GetSpecialType(SpecialType.System_Delegate); private INamedTypeSymbol? _DelegateType; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index e6ed81f419539b..b13761f4bf299f 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -47,6 +47,8 @@ private sealed partial class Emitter private const string JsonExceptionTypeRef = "global::System.Text.Json.JsonException"; private const string TypeTypeRef = "global::System.Type"; private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; + private const string UnsafeAccessorTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorAttribute"; + private const string UnsafeAccessorKindTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorKind"; private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; private const string JsonEncodedTextTypeRef = "global::System.Text.Json.JsonEncodedText"; @@ -500,7 +502,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene string creatorInvocation = FormatDefaultConstructorExpr(typeMetadata); string parameterizedCreatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor - ? GetParameterizedCtorInvocationFunc(typeMetadata) + ? GetParameterizedCtorInvocationFunc(contextSpec, typeMetadata) : "null"; string? propInitMethodName = null; @@ -575,7 +577,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene if (propInitMethodName != null) { writer.WriteLine(); - GeneratePropMetadataInitFunc(writer, propInitMethodName, typeMetadata); + GeneratePropMetadataInitFunc(writer, contextSpec, propInitMethodName, typeMetadata); } if (serializeMethodName != null) @@ -590,13 +592,20 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene GenerateCtorParamMetadataInitFunc(writer, ctorParamMetadataInitMethodName, typeMetadata); } + // Generate UnsafeAccessor methods for init-only properties + if (ShouldGenerateMetadata(typeMetadata) && contextSpec.SupportsUnsafeAccessor) + { + writer.WriteLine(); + GenerateUnsafeAccessorMethods(writer, typeMetadata); + } + writer.Indentation--; writer.WriteLine('}'); return CompleteSourceFileAndReturnText(writer); } - private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) + private void GeneratePropMetadataInitFunc(SourceWriter writer, ContextGenerationSpec contextSpec, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; @@ -633,19 +642,43 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && _ => "null" }; - string setterValue = property switch + string setterValue; + if (property.DefaultIgnoreCondition == JsonIgnoreCondition.Always) { - { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null", - { CanUseSetter: true, IsInitOnlySetter: true } - => $"""static (obj, value) => throw new {InvalidOperationExceptionTypeRef}("{ExceptionMessages.InitOnlyPropertySetterNotSupported}")""", - { CanUseSetter: true } when typeGenerationSpec.TypeRef.IsValueType - => $"""static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj).{propertyName} = value!""", - { CanUseSetter: true } - => $"""static (obj, value) => (({declaringTypeFQN})obj).{propertyName} = value!""", - { CanUseSetter: false, HasJsonInclude: true } - => $"""static (obj, value) => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, property.MemberName)}")""", - _ => "null", - }; + setterValue = "null"; + } + else if (property is { CanUseSetter: true, IsInitOnlySetter: true }) + { + // For init-only properties, use UnsafeAccessor on supported TFMs for better performance, + // and fall back to reflection on older targets. This preserves default values + // for init-only properties when they're not specified in the JSON. + if (contextSpec.SupportsUnsafeAccessor) + { + string unsafeAccessorSetter = $"{GetUnsafeAccessorName(property)}(({declaringTypeFQN})obj, value!)"; + setterValue = $"static (obj, value) => {unsafeAccessorSetter}"; + } + else + { + string reflectionSetter = $"typeof({declaringTypeFQN}).GetProperty({FormatStringLiteral(property.MemberName)}, {InstanceMemberBindingFlagsVariableName})!.SetValue(obj, value)"; + setterValue = $"static (obj, value) => {reflectionSetter}"; + } + } + else if (property.CanUseSetter && typeGenerationSpec.TypeRef.IsValueType) + { + setterValue = $"""static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj).{propertyName} = value!"""; + } + else if (property.CanUseSetter) + { + setterValue = $"""static (obj, value) => (({declaringTypeFQN})obj).{propertyName} = value!"""; + } + else if (property.HasJsonInclude) + { + setterValue = $"""static (obj, value) => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, property.MemberName)}")"""; + } + else + { + setterValue = "null"; + } string ignoreConditionNamedArg = property.DefaultIgnoreCondition.HasValue ? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}" @@ -729,8 +762,17 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin { ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs; - int paramCount = parameters.Count + propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter); - Debug.Assert(paramCount > 0); + // Only count required property initializers that don't match constructor parameters. + // Non-required init-only properties are set via reflection setters, not the constructor delegate. + int requiredInitializerCount = propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter && propInit.IsRequired); + int paramCount = parameters.Count + requiredInitializerCount; + + // If there are no parameters to emit, generate an empty array. + if (paramCount == 0) + { + writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}() => global::System.Array.Empty<{JsonParameterInfoValuesTypeRef}>();"); + return; + } writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}() => new {JsonParameterInfoValuesTypeRef}[]"); writer.WriteLine('{'); @@ -757,9 +799,11 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin } } + // Only emit property initializers for required properties. + // Non-required init-only properties preserve default values and are set via reflection setters. foreach (PropertyInitializerGenerationSpec spec in propertyInitializers) { - if (spec.MatchesConstructorParameter) + if (spec.MatchesConstructorParameter || !spec.IsRequired) { continue; } @@ -928,14 +972,42 @@ static void ThrowPropertyNullException(string propertyName) writer.WriteLine('}'); } - private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec) + private static string GetParameterizedCtorInvocationFunc(ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs; + // Check if the type has required members that need UnsafeAccessor to bypass C#'s required member validation + bool hasRequiredMembers = !typeGenerationSpec.ConstructorSetsRequiredParameters && + propertyInitializers.Any(p => p.IsRequired); + const string ArgsVarName = "args"; - StringBuilder sb = new($"static {ArgsVarName} => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + StringBuilder sb = new(); + + if (hasRequiredMembers && contextSpec.SupportsUnsafeAccessor) + { + // Use UnsafeAccessor to invoke the constructor, bypassing required member validation + string ctorAccessorName = GetUnsafeAccessorCtorName(typeGenerationSpec); + sb.Append($"static {ArgsVarName} => {ctorAccessorName}("); + } + else if (hasRequiredMembers) + { + // Fall back to reflection for older TFMs + string argTypes = parameters.Count == 0 + ? EmptyTypeArray + : $$"""new[] {{{string.Join(", ", parameters.Select(p => $"typeof({p.ParameterType.FullyQualifiedName})"))}}}"""; + string argsArray = parameters.Count == 0 + ? "null" + : $$"""new object?[] {{{string.Join(", ", parameters.Select(p => GetParamUnboxing(p.ParameterType, p.ParameterIndex)))}}}"""; + sb.Append($"static {ArgsVarName} => ({typeGenerationSpec.TypeRef.FullyQualifiedName})typeof({typeGenerationSpec.TypeRef.FullyQualifiedName}).GetConstructor({InstanceMemberBindingFlagsVariableName}, binder: null, {argTypes}, modifiers: null)!.Invoke({argsArray})"); + return sb.ToString(); + } + else + { + // Normal constructor invocation without required member concerns + sb.Append($"static {ArgsVarName} => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + } if (parameters.Count > 0) { @@ -950,17 +1022,9 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type sb.Append(')'); - if (propertyInitializers.Count > 0) - { - sb.Append("{ "); - foreach (PropertyInitializerGenerationSpec property in propertyInitializers) - { - sb.Append($"{property.Name} = {GetParamUnboxing(property.ParameterType, property.ParameterIndex)}, "); - } - - sb.Length -= 2; // delete the last ", " token - sb.Append(" }"); - } + // Do not include any properties in the object initializer. + // Both required and init-only properties should be set via their setter delegates + // to preserve their default values when not specified in the JSON. return sb.ToString(); @@ -1480,6 +1544,79 @@ private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaul private static string FormatStringLiteral(string? value) => value is null ? "null" : SymbolDisplay.FormatLiteral(value, quote: true); private static string FormatCharLiteral(char value) => SymbolDisplay.FormatLiteral(value, quote: true); + /// + /// Gets the name of the UnsafeAccessor method for setting an init-only property. + /// + private static string GetUnsafeAccessorName(PropertyGenerationSpec property) + => $"__UnsafeAccessor_Set_{property.DeclaringType.Name}_{property.MemberName}"; + + /// + /// Gets the name of the UnsafeAccessor method for invoking a constructor. + /// + private static string GetUnsafeAccessorCtorName(TypeGenerationSpec typeSpec) + => $"__UnsafeAccessor_Ctor_{typeSpec.TypeRef.Name}"; + + /// + /// Generates UnsafeAccessor methods for init-only properties and constructors in a type. + /// These methods use UnsafeAccessor on .NET 8+ for better performance. + /// + private static void GenerateUnsafeAccessorMethods(SourceWriter writer, TypeGenerationSpec typeGenerationSpec) + { + // Check if we need UnsafeAccessor for the constructor (types with required members) + bool needsCtorAccessor = !typeGenerationSpec.ConstructorSetsRequiredParameters && + typeGenerationSpec.PropertyInitializerSpecs.Any(p => p.IsRequired); + + // Generate constructor accessor if needed + if (needsCtorAccessor) + { + string ctorAccessorName = GetUnsafeAccessorCtorName(typeGenerationSpec); + string typeFQN = typeGenerationSpec.TypeRef.FullyQualifiedName; + ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; + + string paramList = parameters.Count == 0 + ? "" + : string.Join(", ", parameters.Select(p => $"{p.ParameterType.FullyQualifiedName} {p.Name}")); + + writer.WriteLine($""" + [{UnsafeAccessorTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)] + private static extern {typeFQN} {ctorAccessorName}({paramList}); + """); + } + + // Generate property setter accessors for init-only properties + bool hasInitOnlyProperties = false; + foreach (PropertyGenerationSpec property in typeGenerationSpec.PropertyGenSpecs) + { + if (property.CanUseSetter && property.IsInitOnlySetter && property.DefaultIgnoreCondition != JsonIgnoreCondition.Always) + { + hasInitOnlyProperties = true; + break; + } + } + + if (!hasInitOnlyProperties && !needsCtorAccessor) + { + return; + } + + foreach (PropertyGenerationSpec property in typeGenerationSpec.PropertyGenSpecs) + { + if (!property.CanUseSetter || !property.IsInitOnlySetter || property.DefaultIgnoreCondition == JsonIgnoreCondition.Always) + { + continue; + } + + string declaringTypeFQN = property.DeclaringType.FullyQualifiedName; + string propertyTypeFQN = property.PropertyType.FullyQualifiedName; + string accessorName = GetUnsafeAccessorName(property); + + writer.WriteLine($""" + [{UnsafeAccessorTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{property.MemberName}")] + private static extern void {accessorName}({declaringTypeFQN} obj, {propertyTypeFQN} value); + """); + } + } + /// /// Method used to generate JsonTypeInfo given options instance /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 329a369b4a0135..957965d16632a8 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -166,6 +166,7 @@ public Parser(KnownTypeSymbols knownSymbols) Namespace = contextTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, ContextClassDeclarations = classDeclarationList.ToImmutableEquatableArray(), GeneratedOptionsSpec = options, + SupportsUnsafeAccessor = _knownSymbols.UnsafeAccessorAttributeType is not null, }; // Clear the caches of generated metadata between the processing of context classes. @@ -1627,10 +1628,17 @@ private void ProcessMember( } ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(property, constructorParameters); + bool isRequired = property.IsRequired && !constructorSetsRequiredMembers; - if (property.IsRequired || matchingConstructorParameter is null) + // Only use ParameterizedConstructor strategy for required properties. + // Init-only non-required properties can be set via the setter delegate + // using reflection, which preserves their default values when not specified in JSON. + if (isRequired || matchingConstructorParameter is not null) { - constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor; + if (isRequired) + { + constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor; + } var propertyInitializer = new PropertyInitializerGenerationSpec { @@ -1639,6 +1647,7 @@ private void ProcessMember( MatchesConstructorParameter = matchingConstructorParameter is not null, ParameterIndex = matchingConstructorParameter?.ParameterIndex ?? paramCount++, IsNullable = property.PropertyType.CanBeNull && !property.IsSetterNonNullableAnnotation, + IsRequired = isRequired, }; (propertyInitializers ??= new()).Add(propertyInitializer); diff --git a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs index 00c7192c3ae58c..37b43f65e589bd 100644 --- a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs @@ -36,5 +36,10 @@ public sealed record ContextGenerationSpec public required ImmutableEquatableArray ContextClassDeclarations { get; init; } public required SourceGenerationOptionsSpec? GeneratedOptionsSpec { get; init; } + + /// + /// Whether the target framework supports UnsafeAccessor attribute (.NET 8+). + /// + public required bool SupportsUnsafeAccessor { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs index 189dac1784e4f1..bf717ad512ea43 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs @@ -33,5 +33,13 @@ public sealed record PropertyInitializerGenerationSpec public required bool MatchesConstructorParameter { get; init; } public required bool IsNullable { get; init; } + + /// + /// Indicates whether the property is a C# required member. + /// Required members must be set via object initializers, + /// while non-required init-only properties can preserve default values + /// by setting them via reflection-based setters after construction. + /// + public required bool IsRequired { get; init; } } } diff --git a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs index d355651eb5bd9c..a6c1f64538f6ba 100644 --- a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs @@ -172,8 +172,7 @@ public void TypeWithConstructor_JsonPropertyInfo_AssociatedParameter_MatchesCtor [Theory] [InlineData(typeof(ClassWithRequiredMember))] - [InlineData(typeof(ClassWithInitOnlyProperty))] - public void TypeWithRequiredOrInitMember_SourceGen_HasAssociatedParameterInfo(Type type) + public void TypeWithRequiredMember_SourceGen_HasAssociatedParameterInfo(Type type) { JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); JsonPropertyInfo propertyInfo = typeInfo.Properties.Single(); @@ -201,6 +200,20 @@ public void TypeWithRequiredOrInitMember_SourceGen_HasAssociatedParameterInfo(Ty } } + [Theory] + [InlineData(typeof(ClassWithInitOnlyProperty))] + public void TypeWithInitOnlyNonRequiredMember_NoAssociatedParameterInfo(Type type) + { + // Init-only non-required properties should NOT have associated parameter info + // because they are set via reflection-based setters, not the constructor delegate. + // This preserves their default values when not specified in the JSON. + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + JsonPropertyInfo propertyInfo = typeInfo.Properties.Single(); + + JsonParameterInfo? jsonParameter = propertyInfo.AssociatedParameter; + Assert.Null(jsonParameter); + } + [Theory] [InlineData(typeof(ClassWithDefaultCtor))] [InlineData(typeof(StructWithDefaultCtor))] diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs index 4cacc627710be2..9cca77d34d4936 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs @@ -192,5 +192,66 @@ public RecordWithIgnoredNestedInitOnlyProperty(int foo) public record InnerRecord(int Foo, string Bar); } + + [Fact] + public virtual async Task InitOnlyProperties_PreserveDefaultValues() + { + // Regression test for https://github.com/dotnet/runtime/issues/58770 + // and https://github.com/dotnet/runtime/issues/87488 + // Default values for init-only properties should be preserved when not specified in JSON. + + // When no properties are specified, default values should be used + ClassWithInitOnlyPropertyDefaults obj = await Serializer.DeserializeWrapper("{}"); + Assert.Equal("DefaultName", obj.Name); + Assert.Equal(42, obj.Number); + + // When only some properties are specified, unspecified ones should retain defaults + obj = await Serializer.DeserializeWrapper(@"{""Name"":""Custom""}"); + Assert.Equal("Custom", obj.Name); + Assert.Equal(42, obj.Number); + + obj = await Serializer.DeserializeWrapper(@"{""Number"":100}"); + Assert.Equal("DefaultName", obj.Name); + Assert.Equal(100, obj.Number); + + // When all properties are specified, they should be used + obj = await Serializer.DeserializeWrapper(@"{""Name"":""Custom"",""Number"":100}"); + Assert.Equal("Custom", obj.Name); + Assert.Equal(100, obj.Number); + } + + [Fact] + public virtual async Task InitOnlyProperties_PreserveDefaultValues_Struct() + { + // Regression test for value types with init-only properties with default values + + // When no properties are specified, default values should be used + StructWithInitOnlyPropertyDefaults obj = await Serializer.DeserializeWrapper("{}"); + Assert.Equal("DefaultName", obj.Name); + Assert.Equal(42, obj.Number); + + // When only some properties are specified, unspecified ones should retain defaults + obj = await Serializer.DeserializeWrapper(@"{""Name"":""Custom""}"); + Assert.Equal("Custom", obj.Name); + Assert.Equal(42, obj.Number); + + // When all properties are specified, they should be used + obj = await Serializer.DeserializeWrapper(@"{""Name"":""Custom"",""Number"":100}"); + Assert.Equal("Custom", obj.Name); + Assert.Equal(100, obj.Number); + } + + public class ClassWithInitOnlyPropertyDefaults + { + public string Name { get; init; } = "DefaultName"; + public int Number { get; init; } = 42; + } + + public struct StructWithInitOnlyPropertyDefaults + { + public StructWithInitOnlyPropertyDefaults() { } + public string Name { get; init; } = "DefaultName"; + public int Number { get; init; } = 42; + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs index 5c7a880870980d..a87ab2a63b0c42 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs @@ -198,6 +198,8 @@ public override async Task ClassWithIgnoredAndPrivateMembers_DoesNotIncludeIgnor [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))] [JsonSerializable(typeof(ClassWithMissingObjectProperty))] [JsonSerializable(typeof(ClassWithInitOnlyProperty))] + [JsonSerializable(typeof(ClassWithInitOnlyPropertyDefaults))] + [JsonSerializable(typeof(StructWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(Class_WithIgnoredInitOnlyProperty))] [JsonSerializable(typeof(Record_WithIgnoredPropertyInCtor))] [JsonSerializable(typeof(Class_WithIgnoredRequiredProperty))] @@ -477,6 +479,8 @@ partial class DefaultContextWithGlobalIgnoreSetting : JsonSerializerContext; [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))] [JsonSerializable(typeof(ClassWithMissingObjectProperty))] [JsonSerializable(typeof(ClassWithInitOnlyProperty))] + [JsonSerializable(typeof(ClassWithInitOnlyPropertyDefaults))] + [JsonSerializable(typeof(StructWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(Class_WithIgnoredInitOnlyProperty))] [JsonSerializable(typeof(Record_WithIgnoredPropertyInCtor))] [JsonSerializable(typeof(Class_WithIgnoredRequiredProperty))]