From 1ea4ec249fb14f66c779ad24047d004b6f3cd912 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:45:55 +0000 Subject: [PATCH 1/3] Add parameterless [JsonSerializable] attribute and POCO source generation pipeline - Extended JsonSerializableAttribute with parameterless ctor and AttributeTargets.Struct - Updated ref assembly - Added SYSLIB1227 (type must be partial) and SYSLIB1228 (member name collision) diagnostics - Added PocoTypeGenerationSpec model - Added second pipeline in Initialize() for POCO types - Added EmitPocoType() to generate backing context and static JsonTypeInfo property - Updated Roslyn 3.11 generator with equivalent POCO support - Handled parameterless [JsonSerializable] in ParseJsonSerializableAttribute Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d115f32b-7c2c-4cca-a15d-d57ce9850f97 Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- docs/project/list-of-diagnostics.md | 4 +- .../Common/JsonSerializableAttribute.cs | 17 +- ...onSourceGenerator.DiagnosticDescriptors.cs | 16 ++ .../gen/JsonSourceGenerator.Emitter.cs | 81 ++++++++++ .../gen/JsonSourceGenerator.Parser.cs | 147 +++++++++++++++++- .../gen/JsonSourceGenerator.Roslyn3.11.cs | 132 +++++++++++----- .../gen/JsonSourceGenerator.Roslyn4.0.cs | 64 ++++++++ .../gen/Model/PocoTypeGenerationSpec.cs | 60 +++++++ .../gen/Resources/Strings.resx | 12 ++ .../gen/Resources/xlf/Strings.cs.xlf | 20 +++ .../gen/Resources/xlf/Strings.de.xlf | 20 +++ .../gen/Resources/xlf/Strings.es.xlf | 20 +++ .../gen/Resources/xlf/Strings.fr.xlf | 20 +++ .../gen/Resources/xlf/Strings.it.xlf | 20 +++ .../gen/Resources/xlf/Strings.ja.xlf | 20 +++ .../gen/Resources/xlf/Strings.ko.xlf | 20 +++ .../gen/Resources/xlf/Strings.pl.xlf | 20 +++ .../gen/Resources/xlf/Strings.pt-BR.xlf | 20 +++ .../gen/Resources/xlf/Strings.ru.xlf | 20 +++ .../gen/Resources/xlf/Strings.tr.xlf | 20 +++ .../gen/Resources/xlf/Strings.zh-Hans.xlf | 20 +++ .../gen/Resources/xlf/Strings.zh-Hant.xlf | 20 +++ .../System.Text.Json.SourceGeneration.targets | 1 + .../System.Text.Json/ref/System.Text.Json.cs | 3 +- 24 files changed, 748 insertions(+), 49 deletions(-) create mode 100644 src/libraries/System.Text.Json/gen/Model/PocoTypeGenerationSpec.cs diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 306e9f9d8f2e83..ca4af89e232f27 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -272,8 +272,8 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1224`__ | Types annotated with JsonSerializableAttribute must be classes deriving from JsonSerializerContext. | | __`SYSLIB1225`__ | Type includes ref like property, field or constructor parameter. | | __`SYSLIB1226`__ | 'JsonIgnoreCondition.Always' is not valid on type-level 'JsonIgnoreAttribute' annotations. | -| __`SYSLIB1227`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | -| __`SYSLIB1228`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1227`__ | Types annotated with the parameterless JsonSerializableAttribute must be partial. | +| __`SYSLIB1228`__ | Type already contains a member named 'JsonTypeInfo'. | | __`SYSLIB1229`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | | __`SYSLIB1230`__ | Deriving from a `GeneratedComInterface`-attributed interface defined in another assembly is not supported. | | __`SYSLIB1231`__ | _`SYSLIB1230`-`SYSLIB1239` reserved for Microsoft.Interop.ComInterfaceGenerator._ | diff --git a/src/libraries/System.Text.Json/Common/JsonSerializableAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSerializableAttribute.cs index 6b2535269dc527..4b77451ce714d2 100644 --- a/src/libraries/System.Text.Json/Common/JsonSerializableAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSerializableAttribute.cs @@ -11,7 +11,12 @@ namespace System.Text.Json.Serialization /// Instructs the System.Text.Json source generator to generate source code to help optimize performance /// when serializing and deserializing instances of the specified type and types in its object graph. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + /// + /// This attribute can be applied to a -derived class to register types + /// for source generation, or directly to a partial class or struct to enable simplified source generation + /// with an auto-generated context and a static JsonTypeInfo property. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)] #if BUILDING_SOURCE_GENERATOR internal @@ -28,6 +33,16 @@ sealed class JsonSerializableAttribute : JsonAttribute public JsonSerializableAttribute(Type type) { } #pragma warning restore IDE0060 + /// + /// Initializes a new instance of when applied directly to the type to be serialized. + /// + /// + /// When this parameterless constructor is used on a partial class or struct, the source generator will + /// automatically generate a backing and a static JsonTypeInfo + /// property on the annotated type. + /// + public JsonSerializableAttribute() { } + /// /// The name of the property for the generated for /// the type on the generated, derived type. diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs index a926b75a5c1631..108126f2a1329e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs @@ -131,6 +131,22 @@ internal static class DiagnosticDescriptors category: JsonConstants.SystemTextJsonSourceGenerationName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static DiagnosticDescriptor JsonSerializableTypeMustBePartial { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1227", + title: new LocalizableResourceString(nameof(SR.JsonSerializableTypeMustBePartialTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.JsonSerializableTypeMustBePartialMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static DiagnosticDescriptor JsonSerializableTypeHasJsonTypeInfoMember { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1228", + title: new LocalizableResourceString(nameof(SR.JsonSerializableTypeHasJsonTypeInfoMemberTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.JsonSerializableTypeHasJsonTypeInfoMemberMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index d30851a469dc1b..fbb00130201a1e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -145,6 +145,87 @@ public void Emit(ContextGenerationSpec contextGenerationSpec) _typeIndex.Clear(); } + /// + /// Emits source code for a type annotated with the parameterless [JsonSerializable] attribute. + /// Generates a file-scoped backing JsonSerializerContext and a partial type extension + /// with a static JsonTypeInfo property. + /// + public void EmitPocoType(PocoTypeGenerationSpec pocoSpec) + { + var writer = new SourceWriter(); + + writer.WriteLine(""" + // + + #nullable enable annotations + #nullable disable warnings + + // Suppress warnings about [Obsolete] member usage in generated code. + #pragma warning disable CS0612, CS0618 + + """); + + if (pocoSpec.Namespace != null) + { + writer.WriteLine($"namespace {pocoSpec.Namespace}"); + writer.WriteLine('{'); + writer.Indentation++; + } + + // Emit containing type declarations + ImmutableEquatableArray typeDeclarations = pocoSpec.TypeDeclarations; + Debug.Assert(typeDeclarations.Count > 0); + + for (int i = typeDeclarations.Count - 1; i > 0; i--) + { + writer.WriteLine(typeDeclarations[i]); + writer.WriteLine('{'); + writer.Indentation++; + } + + // Emit the partial type declaration with the static property + writer.WriteLine($"""[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{s_assemblyName.Name}", "{s_assemblyName.Version}")]"""); + writer.WriteLine(typeDeclarations[0]); + writer.WriteLine('{'); + writer.Indentation++; + + string fullyQualifiedTypeName = pocoSpec.TypeRef.FullyQualifiedName; + + writer.WriteLine($$""" + /// + /// Gets the generated for . + /// + public static global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<{{fullyQualifiedTypeName}}> {{pocoSpec.TypeInfoPropertyName}} => + {{pocoSpec.GeneratedContextName}}.Default.{{pocoSpec.TypeName}}; + """); + + writer.Indentation--; + writer.WriteLine('}'); + + // Close containing type declarations + for (int i = 1; i < typeDeclarations.Count; i++) + { + writer.Indentation--; + writer.WriteLine('}'); + } + + // Emit the file-scoped backing context class + writer.WriteLine(); + writer.WriteLine($"""[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{s_assemblyName.Name}", "{s_assemblyName.Version}")]"""); + writer.WriteLine($"[global::System.Text.Json.Serialization.JsonSerializable(typeof({fullyQualifiedTypeName}))]"); + writer.WriteLine($"file sealed partial class {pocoSpec.GeneratedContextName} : global::System.Text.Json.Serialization.JsonSerializerContext"); + writer.WriteLine('{'); + writer.WriteLine('}'); + + if (pocoSpec.Namespace != null) + { + writer.Indentation--; + writer.WriteLine('}'); + } + + AddSource($"{pocoSpec.TypeName}.JsonSerializable.g.cs", writer.ToSourceText()); + } + private static SourceWriter CreateSourceWriterWithContextHeader(ContextGenerationSpec contextSpec, bool isPrimaryContextSourceFile = false, string? interfaceImplementation = null) { var writer = new SourceWriter(); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 5428245c7ce1b1..c3f85d4a8c6b25 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -106,7 +106,14 @@ public Parser(KnownTypeSymbols knownSymbols) if (!_knownSymbols.JsonSerializerContextType.IsAssignableFrom(contextTypeSymbol)) { - ReportDiagnostic(DiagnosticDescriptors.JsonSerializableAttributeOnNonContextType, _contextClassLocation, contextTypeSymbol.ToDisplayString()); + // Only emit SYSLIB1224 if the type has [JsonSerializable(typeof(T))] attributes + // (i.e., the old-style usage on a non-context type). Parameterless [JsonSerializable] + // on a non-context type is handled by the POCO pipeline. + if (!HasOnlyParameterlessJsonSerializableAttributes(contextTypeSymbol)) + { + ReportDiagnostic(DiagnosticDescriptors.JsonSerializableAttributeOnNonContextType, _contextClassLocation, contextTypeSymbol.ToDisplayString()); + } + return null; } @@ -184,7 +191,137 @@ public Parser(KnownTypeSymbols knownSymbols) return contextGenSpec; } - private static bool TryGetNestedTypeDeclarations(ClassDeclarationSyntax contextClassSyntax, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out List? typeDeclarations) + /// + /// Checks whether all [JsonSerializable] attributes on the type are parameterless (POCO-style). + /// + private bool HasOnlyParameterlessJsonSerializableAttributes(INamedTypeSymbol typeSymbol) + { + Debug.Assert(_knownSymbols.JsonSerializableAttributeType != null); + + foreach (AttributeData attributeData in typeSymbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, _knownSymbols.JsonSerializableAttributeType)) + { + if (attributeData.ConstructorArguments.Length > 0) + { + return false; + } + } + } + + return true; + } + + /// + /// Parses a type declaration annotated with the parameterless [JsonSerializable] attribute. + /// + public PocoTypeGenerationSpec? ParsePocoTypeGenerationSpec(TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken) + { + if (!_compilationContainsCoreJsonTypes) + { + return null; + } + + Debug.Assert(_knownSymbols.JsonSerializableAttributeType != null); + + INamedTypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken); + if (typeSymbol is null) + { + return null; + } + + Location? location = typeSymbol.GetLocation(); + + // Skip if this type derives from JsonSerializerContext — that's handled by the existing pipeline + if (_knownSymbols.JsonSerializerContextType != null && + _knownSymbols.JsonSerializerContextType.IsAssignableFrom(typeSymbol)) + { + return null; + } + + // Find the parameterless [JsonSerializable] attribute + AttributeData? parameterlessAttribute = null; + foreach (AttributeData attributeData in typeSymbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, _knownSymbols.JsonSerializableAttributeType) && + attributeData.ConstructorArguments.Length == 0) + { + parameterlessAttribute = attributeData; + break; + } + } + + if (parameterlessAttribute is null) + { + return null; + } + + // Validate language version + LanguageVersion? langVersion = _knownSymbols.Compilation.GetLanguageVersion(); + if (langVersion is null or < MinimumSupportedLanguageVersion) + { + Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.JsonUnsupportedLanguageVersion, + location, langVersion?.ToDisplayString(), MinimumSupportedLanguageVersion.ToDisplayString())); + return null; + } + + // Validate that the type and all containing types are partial + if (!TryGetNestedTypeDeclarations(typeDeclaration, semanticModel, cancellationToken, out List? typeDeclarations)) + { + Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.JsonSerializableTypeMustBePartial, + location, typeSymbol.ToDisplayString())); + return null; + } + + // Parse named arguments from the attribute + string? typeInfoPropertyName = null; + JsonSourceGenerationMode? generationMode = null; + + foreach (KeyValuePair namedArg in parameterlessAttribute.NamedArguments) + { + switch (namedArg.Key) + { + case nameof(JsonSerializableAttribute.TypeInfoPropertyName): + typeInfoPropertyName = (string)namedArg.Value.Value!; + break; + case nameof(JsonSerializableAttribute.GenerationMode): + generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!; + break; + } + } + + // Default property name is "JsonTypeInfo" + string propertyName = typeInfoPropertyName ?? "JsonTypeInfo"; + + // Check for member name collision + foreach (ISymbol member in typeSymbol.GetMembers(propertyName)) + { + // Only report collision if the member is not compiler-generated (i.e., not from our previous generation) + if (!member.IsImplicitlyDeclared) + { + Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.JsonSerializableTypeHasJsonTypeInfoMember, + location, typeSymbol.ToDisplayString())); + return null; + } + } + + string typeName = typeSymbol.Name; + string generatedContextName = $"__JsonContext_{typeName}"; + + return new PocoTypeGenerationSpec + { + TypeRef = new(typeSymbol), + TypeName = typeName, + TypeInfoPropertyName = propertyName, + Namespace = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, + TypeDeclarations = typeDeclarations.ToImmutableEquatableArray(), + GeneratedContextName = generatedContextName, + GenerationMode = generationMode, + IsValueType = typeSymbol.IsValueType, + }; + } + + private static bool TryGetNestedTypeDeclarations(TypeDeclarationSyntax contextClassSyntax, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out List? typeDeclarations) { typeDeclarations = null; @@ -523,6 +660,12 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN private TypeToGenerate? ParseJsonSerializableAttribute(AttributeData attributeData) { + // Skip parameterless [JsonSerializable] attributes (POCO-style) — they're handled by the POCO pipeline. + if (attributeData.ConstructorArguments.Length == 0) + { + return null; + } + Debug.Assert(attributeData.ConstructorArguments.Length == 1); var typeSymbol = (ITypeSymbol?)attributeData.ConstructorArguments[0].Value; if (typeSymbol is null) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs index 5397715ede9da8..934a94c40e8e91 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs @@ -52,45 +52,68 @@ public void Execute(GeneratorExecutionContext executionContext) CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; try { - if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.ContextClassDeclarations == null) + if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver) { // nothing to do yet return; } - // Stage 1. Parse the identified JsonSerializerContext classes and store the model types. KnownTypeSymbols knownSymbols = new(executionContext.Compilation); - Parser parser = new(knownSymbols); - List? contextGenerationSpecs = null; - foreach ((ClassDeclarationSyntax? contextClassDeclaration, SemanticModel semanticModel) in receiver.ContextClassDeclarations) + // Stage 1a. Parse the identified JsonSerializerContext classes and store the model types. + if (receiver.ContextClassDeclarations != null) { - ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(contextClassDeclaration, semanticModel, executionContext.CancellationToken); - if (contextGenerationSpec is null) + Parser parser = new(knownSymbols); + + List? contextGenerationSpecs = null; + foreach ((ClassDeclarationSyntax? contextClassDeclaration, SemanticModel semanticModel) in receiver.ContextClassDeclarations) { - continue; + ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(contextClassDeclaration, semanticModel, executionContext.CancellationToken); + if (contextGenerationSpec is null) + { + continue; + } + + (contextGenerationSpecs ??= new()).Add(contextGenerationSpec); } - (contextGenerationSpecs ??= new()).Add(contextGenerationSpec); - } + // Report any diagnostics gathered by the parser. + foreach (Diagnostic diagnostic in parser.Diagnostics) + { + executionContext.ReportDiagnostic(diagnostic); + } - // Stage 2. Report any diagnostics gathered by the parser. - foreach (Diagnostic diagnostic in parser.Diagnostics) - { - executionContext.ReportDiagnostic(diagnostic); + if (contextGenerationSpecs is not null) + { + // Emit source code from the spec models. + OnSourceEmitting?.Invoke(contextGenerationSpecs.ToImmutableArray()); + Emitter emitter = new(executionContext); + foreach (ContextGenerationSpec contextGenerationSpec in contextGenerationSpecs) + { + emitter.Emit(contextGenerationSpec); + } + } } - if (contextGenerationSpecs is null) + // Stage 1b. Parse POCO types annotated with parameterless [JsonSerializable]. + if (receiver.PocoTypeDeclarations != null) { - return; - } + Parser parser = new(knownSymbols); - // Stage 3. Emit source code from the spec models. - OnSourceEmitting?.Invoke(contextGenerationSpecs.ToImmutableArray()); - Emitter emitter = new(executionContext); - foreach (ContextGenerationSpec contextGenerationSpec in contextGenerationSpecs) - { - emitter.Emit(contextGenerationSpec); + foreach ((TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel) in receiver.PocoTypeDeclarations) + { + PocoTypeGenerationSpec? pocoSpec = parser.ParsePocoTypeGenerationSpec(typeDeclaration, semanticModel, executionContext.CancellationToken); + if (pocoSpec is not null) + { + Emitter emitter = new(executionContext); + emitter.EmitPocoType(pocoSpec); + } + } + + foreach (Diagnostic diagnostic in parser.Diagnostics) + { + executionContext.ReportDiagnostic(diagnostic); + } } } finally @@ -109,48 +132,71 @@ public SyntaxContextReceiver(CancellationToken cancellationToken) } public List<(ClassDeclarationSyntax, SemanticModel)>? ContextClassDeclarations { get; private set; } + public List<(TypeDeclarationSyntax, SemanticModel)>? PocoTypeDeclarations { get; private set; } public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { - if (IsSyntaxTargetForGeneration(context.Node)) + if (context.Node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } typeDeclaration) { - ClassDeclarationSyntax? classSyntax = GetSemanticTargetForGeneration(context, _cancellationToken); - if (classSyntax != null) + if (typeDeclaration is ClassDeclarationSyntax { BaseList.Types.Count: > 0 } classDeclaration) + { + // Could be a JsonSerializerContext-derived class + if (HasJsonSerializableAttribute(context, classDeclaration)) + { + (ContextClassDeclarations ??= new()).Add((classDeclaration, context.SemanticModel)); + } + } + + // Also check for parameterless [JsonSerializable] on any type (POCO pattern) + if (HasParameterlessJsonSerializableAttribute(context, typeDeclaration)) { - (ContextClassDeclarations ??= new()).Add((classSyntax, context.SemanticModel)); + (PocoTypeDeclarations ??= new()).Add((typeDeclaration, context.SemanticModel)); } } } - private static bool IsSyntaxTargetForGeneration(SyntaxNode node) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0, BaseList.Types.Count: > 0 }; - - private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken cancellationToken) + private static bool HasJsonSerializableAttribute(GeneratorSyntaxContext context, ClassDeclarationSyntax classDeclaration) { - var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; - - foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists) + foreach (AttributeListSyntax attributeListSyntax in classDeclaration.AttributeLists) { foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) { - cancellationToken.ThrowIfCancellationRequested(); - - IMethodSymbol? attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol as IMethodSymbol; - if (attributeSymbol == null) + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is IMethodSymbol attributeSymbol) { - continue; + string fullName = attributeSymbol.ContainingType.ToDisplayString(); + if (fullName == Parser.JsonSerializableAttributeFullName) + { + return true; + } } + } + } - INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType; - string fullName = attributeContainingTypeSymbol.ToDisplayString(); + return false; + } - if (fullName == Parser.JsonSerializableAttributeFullName) + private static bool HasParameterlessJsonSerializableAttribute(GeneratorSyntaxContext context, TypeDeclarationSyntax typeDeclaration) + { + foreach (AttributeListSyntax attributeListSyntax in typeDeclaration.AttributeLists) + { + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + // Parameterless: no argument list, or empty argument list + if (attributeSyntax.ArgumentList is null or { Arguments.Count: 0 }) { - return classDeclarationSyntax; + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is IMethodSymbol attributeSymbol) + { + string fullName = attributeSymbol.ContainingType.ToDisplayString(); + if (fullName == Parser.JsonSerializableAttributeFullName) + { + return true; + } + } } } } - return null; + return false; } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs index 79fab10a1cfc34..9708ade2da8333 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs @@ -31,6 +31,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IncrementalValueProvider knownTypeSymbols = context.CompilationProvider .Select((compilation, _) => new KnownTypeSymbols(compilation)); + // Pipeline 1: Existing context-based source generation IncrementalValuesProvider<(ContextGenerationSpec?, ImmutableArray)> contextGenerationSpecs = context.SyntaxProvider .ForAttributeWithMetadataName( #if !ROSLYN4_4_OR_GREATER @@ -88,6 +89,47 @@ public void Initialize(IncrementalGeneratorInitializationContext context) contextGenerationSpecs.Select(static (t, _) => t.Item2); context.RegisterSourceOutput(diagnostics, EmitDiagnostics); + + // Pipeline 2: POCO-based source generation (parameterless [JsonSerializable] on data types) + IncrementalValuesProvider<(PocoTypeGenerationSpec?, ImmutableArray)> pocoGenerationSpecs = context.SyntaxProvider + .ForAttributeWithMetadataName( +#if !ROSLYN4_4_OR_GREATER + context, +#endif + Parser.JsonSerializableAttributeFullName, + (node, _) => node is TypeDeclarationSyntax, + (context, _) => (TypeDeclaration: (TypeDeclarationSyntax)context.TargetNode, context.SemanticModel)) + .Combine(knownTypeSymbols) + .Select(static (tuple, cancellationToken) => + { +#pragma warning disable RS1035 + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try + { +#pragma warning restore RS1035 + Parser parser = new(tuple.Right); + PocoTypeGenerationSpec? pocoSpec = parser.ParsePocoTypeGenerationSpec(tuple.Left.TypeDeclaration, tuple.Left.SemanticModel, cancellationToken); + ImmutableArray pocoDiagnostics = parser.Diagnostics.ToImmutableArray(); + return (pocoSpec, pocoDiagnostics); +#pragma warning disable RS1035 + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } +#pragma warning restore RS1035 + }); + + IncrementalValuesProvider pocoSpecs = + pocoGenerationSpecs.Select(static (t, _) => t.Item1); + + context.RegisterSourceOutput(pocoSpecs, EmitPocoSource); + + IncrementalValuesProvider> pocoDiagnostics = + pocoGenerationSpecs.Select(static (t, _) => t.Item2); + + context.RegisterSourceOutput(pocoDiagnostics, EmitDiagnostics); } private void EmitSource(SourceProductionContext sourceProductionContext, ContextGenerationSpec? contextGenerationSpec) @@ -125,6 +167,28 @@ private static void EmitDiagnostics(SourceProductionContext context, ImmutableAr } } + private static void EmitPocoSource(SourceProductionContext sourceProductionContext, PocoTypeGenerationSpec? pocoSpec) + { + if (pocoSpec is null) + { + return; + } + +#pragma warning disable RS1035 + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try + { + Emitter emitter = new(sourceProductionContext); + emitter.EmitPocoType(pocoSpec); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } +#pragma warning restore RS1035 + } + /// /// Instrumentation helper for unit tests. /// diff --git a/src/libraries/System.Text.Json/gen/Model/PocoTypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PocoTypeGenerationSpec.cs new file mode 100644 index 00000000000000..af178ce88ac6ff --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Model/PocoTypeGenerationSpec.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization; +using SourceGenerators; + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Represents a type annotated with the parameterless [JsonSerializable] attribute. + /// The source generator will produce a backing JsonSerializerContext and a static + /// JsonTypeInfo property on the annotated partial type. + /// + [DebuggerDisplay("Type = {TypeRef.FullyQualifiedName}")] + public sealed record PocoTypeGenerationSpec + { + /// + /// The fully qualified name and metadata of the annotated type. + /// + public required TypeRef TypeRef { get; init; } + + /// + /// The short (unqualified) name of the annotated type. + /// + public required string TypeName { get; init; } + + /// + /// The name to use for the generated static JsonTypeInfo property. + /// Defaults to "JsonTypeInfo" unless overridden by TypeInfoPropertyName. + /// + public required string TypeInfoPropertyName { get; init; } + + /// + /// The namespace of the annotated type, or null for global namespace. + /// + public required string? Namespace { get; init; } + + /// + /// The type declaration strings (including modifiers, keyword, name) + /// for the annotated type and all containing types. + /// + public required ImmutableEquatableArray TypeDeclarations { get; init; } + + /// + /// The generated context class name (e.g., "__JsonContext_WeatherForecast"). + /// + public required string GeneratedContextName { get; init; } + + /// + /// The generation mode requested by the attribute. + /// + public required JsonSourceGenerationMode? GenerationMode { get; init; } + + /// + /// Whether the annotated type is a value type (struct). + /// + public required bool IsValueType { get; init; } + } +} diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx index 71ed2ca15e21bf..dd6b9e45ed3127 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -207,4 +207,16 @@ The type '{0}' has been annotated with 'JsonIgnoreAttribute' using 'JsonIgnoreCondition.Always' which is not valid on type declarations. The attribute will be ignored. + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + Type already contains a member named 'JsonTypeInfo'. + + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf index 79b8cd0d6dd8ba..cc6afdc852a497 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf @@ -92,6 +92,26 @@ Typy anotované atributem JsonSerializableAttribute musí být třídy odvozené od třídy JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Člen {0} byl opatřen poznámkou jsonStringEnumConverter, což není v nativním AOT podporováno. Zvažte použití obecného objektu JsonStringEnumConverter<TEnum>. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf index 52b5e98696508a..9562197eed9476 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf @@ -92,6 +92,26 @@ Mit JsonSerializableAttribute kommentierte Typen müssen Klassen sein, die von JsonSerializerContext abgeleitet werden. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Der Member "{0}" wurde mit "JsonStringEnumConverter" kommentiert, was in nativem AOT nicht unterstützt wird. Erwägen Sie stattdessen die Verwendung des generischen "JsonStringEnumConverter<TEnum>". diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf index 0c75870edb1d11..958cdb7b1e0b4e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf @@ -92,6 +92,26 @@ Los tipos anotados con JsonSerializableAttribute deben ser clases derivadas de JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. El miembro '{0}' se ha anotado con 'JsonStringEnumConverter', que no se admite en AOT nativo. Considere la posibilidad de usar el elemento genérico "JsonStringEnumConverter<TEnum>" en su lugar. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf index 6ece010bcfd65a..cd04554edd3494 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf @@ -92,6 +92,26 @@ Les types annotés avec l'attribut JsonSerializable doivent être des classes dérivant de JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Le membre '{0}' a été annoté avec 'JsonStringEnumConverter', ce qui n’est pas pris en charge dans AOT natif. Utilisez plutôt le générique 'JsonStringEnumConverter<TEnum>'. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf index a542b8017265e5..0adbc25f7c36e5 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf @@ -92,6 +92,26 @@ I tipi annotati con JsonSerializableAttribute devono essere classi che derivano da JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Il membro '{0}' è stato annotato con 'JsonStringEnumConverter' che non è supportato in AOT nativo. Provare a usare il generico 'JsonStringEnumConverter<TEnum>'. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf index 27f29423ff6a5f..a26b4bee1e6756 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf @@ -92,6 +92,26 @@ JsonSerializableAttribute で注釈が付けられた型は、JsonSerializerContext から派生するクラスである必要があります。 + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. メンバー '{0}' には、ネイティブ AOT ではサポートされていない 'JsonStringEnumConverter' の注釈が付けられています。 代わりに汎用の 'JsonStringEnumConverter<TEnum>' を使用することを検討してください。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf index 8832eb4d85a761..5be4d956865d64 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf @@ -92,6 +92,26 @@ JsonSerializableAttribute 주석이 추가된 형식은 JsonSerializerContext에서 파생된 클래스여야 합니다. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. '{0}' 멤버에 네이티브 AOT에서 지원되지 않는 'JsonStringEnumConverter'로 주석이 달렸습니다. 대신 제네릭 'JsonStringEnumConverter<TEnum>'을 사용해 보세요. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf index 9cdc8b227b3532..92c3802d30f66d 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf @@ -92,6 +92,26 @@ Typy z adnotacjami JsonSerializableAttribute muszą być klasami pochodzącymi z elementu JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Element członkowski '{0}' został opatrzony adnotacją 'JsonStringEnumConverter', która nie jest obsługiwana w natywnym AOT. Zamiast tego należy rozważyć użycie ogólnego konwertera „JsonStringEnumConverter<TEnum>”. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf index 3b74ad1d07dfde..51220c0fa31b34 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf @@ -92,6 +92,26 @@ Tipos anotados com JsonSerializable Attribute devem ser classes derivadas de JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. O membro "{0}" foi anotado com "JsonStringEnumConverter" que não tem suporte na AOT nativa. Considere usar o genérico "JsonStringEnumConverter<TEnum>". diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf index bd9e003fac6361..fe51831e0e6b73 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf @@ -92,6 +92,26 @@ Типы, аннотированные атрибутом JsonSerializableAttribute, должны быть классами, производными от JsonSerializerContext. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. Элемент "{0}" содержит примечание JsonStringEnumConverter, что не поддерживается в собственном AOT. Вместо этого рассмотрите возможность использовать универсальный параметр JsonStringEnumConverter<TEnum>. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf index 0757cda7d71717..62e2028b8e8ef0 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf @@ -92,6 +92,26 @@ JsonSerializableAttribute ile not eklenen türler, JsonSerializerContext'ten türetilen sınıflar olmalıdır. + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. '{0}' adlı üyeye yerel AOT’de desteklenmeyen 'JsonStringEnumConverter' parametresi eklendi. bunun yerine genel 'JsonStringEnumConverter<TEnum>' parametresini kullanmayı deneyin. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf index 63e0251fec99f5..e6795fa7f9006f 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -92,6 +92,26 @@ 使用 JsonSerializableAttribute 批注的类型必须是派生自 JsonSerializerContext 的类。 + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. 成员“{0}”已使用本机 AOT 中不支持的 "JsonStringEnumConverter" 进行批注。请改为考虑使用泛型 "JsonStringEnumConverter<TEnum>"。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf index 95061df16ecbb5..9d0105c9824aa8 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -92,6 +92,26 @@ 以 JsonSerializableAttribute 標註的類型必須為衍生自 JsonSerializerCoNtext 的類別。 + + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + The type '{0}' already contains a member named 'JsonTypeInfo'. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to specify a different name for the generated property. + + + + Type already contains a member named 'JsonTypeInfo'. + Type already contains a member named 'JsonTypeInfo'. + + + + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + The type '{0}' has been annotated with the parameterless JsonSerializableAttribute but is not declared as partial. The type and all containing types must be made partial to kick off source generation. + + + + Types annotated with the parameterless JsonSerializableAttribute must be partial. + Types annotated with the parameterless JsonSerializableAttribute must be partial. + + The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead. 成員 '{0}' 已使用原生 AOT 不支援的 'JsonStringEnumConverter' 加上標註。請考慮改用一般 'JsonStringEnumConverter<TEnum>'。 diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 0c7c4cead803df..58a2ea4b9db82b 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -73,6 +73,7 @@ + diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 07091b502b1143..c4ae3b4b7f500a 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -1152,10 +1152,11 @@ public sealed partial class JsonRequiredAttribute : System.Text.Json.Serializati { public JsonRequiredAttribute() { } } - [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true)] + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Struct, AllowMultiple=true)] public sealed partial class JsonSerializableAttribute : System.Text.Json.Serialization.JsonAttribute { public JsonSerializableAttribute(System.Type type) { } + public JsonSerializableAttribute() { } public System.Text.Json.Serialization.JsonSourceGenerationMode GenerationMode { get { throw null; } set { } } public string? TypeInfoPropertyName { get { throw null; } set { } } } From 9254de516ed312a7b4c980dc94a7585b939930ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:00:59 +0000 Subject: [PATCH 2/3] Fix POCO source generation: use full type analysis and emit base class declaration - Changed ParsePocoTypeGenerationSpec to return (PocoTypeGenerationSpec, ContextGenerationSpec) tuple - Use existing type graph traversal to build proper ContextGenerationSpec for POCO types - Added synthetic TypeRef constructor for context types not in compilation - Emit base class declaration in separate .Base.g.cs file for synthetic context - All 218 unit tests pass including 7 new POCO tests Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d115f32b-7c2c-4cca-a15d-d57ce9850f97 Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- .../Common/src/SourceGenerators/TypeRef.cs | 12 + .../gen/JsonSourceGenerator.Emitter.cs | 55 +++-- .../gen/JsonSourceGenerator.Parser.cs | 57 ++++- .../gen/JsonSourceGenerator.Roslyn3.11.cs | 9 +- .../gen/JsonSourceGenerator.Roslyn4.0.cs | 17 +- .../JsonSourceGeneratorTests.cs | 214 ++++++++++++++++++ 6 files changed, 331 insertions(+), 33 deletions(-) diff --git a/src/libraries/Common/src/SourceGenerators/TypeRef.cs b/src/libraries/Common/src/SourceGenerators/TypeRef.cs index a4d556ef786dbe..74997a6111a466 100644 --- a/src/libraries/Common/src/SourceGenerators/TypeRef.cs +++ b/src/libraries/Common/src/SourceGenerators/TypeRef.cs @@ -22,6 +22,18 @@ public TypeRef(ITypeSymbol type) SpecialType = type.OriginalDefinition.SpecialType; } + /// + /// Creates a TypeRef for a synthetic type that doesn't exist in the compilation. + /// + public TypeRef(string name, string fullyQualifiedName) + { + Name = name; + FullyQualifiedName = fullyQualifiedName; + IsValueType = false; + TypeKind = TypeKind.Class; + SpecialType = SpecialType.None; + } + public string Name { get; } /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index fbb00130201a1e..7458c969786073 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -146,12 +146,13 @@ public void Emit(ContextGenerationSpec contextGenerationSpec) } /// - /// Emits source code for a type annotated with the parameterless [JsonSerializable] attribute. - /// Generates a file-scoped backing JsonSerializerContext and a partial type extension - /// with a static JsonTypeInfo property. + /// Emits the partial type extension with a static JsonTypeInfo property + /// for a type annotated with the parameterless [JsonSerializable] attribute. + /// Also emits the base class declaration for the synthetic backing context. /// - public void EmitPocoType(PocoTypeGenerationSpec pocoSpec) + public void EmitPocoTypeProperty(PocoTypeGenerationSpec pocoSpec) { + // 1. Emit the partial type with the static JsonTypeInfo property var writer = new SourceWriter(); writer.WriteLine(""" @@ -199,31 +200,47 @@ public void EmitPocoType(PocoTypeGenerationSpec pocoSpec) {{pocoSpec.GeneratedContextName}}.Default.{{pocoSpec.TypeName}}; """); - writer.Indentation--; - writer.WriteLine('}'); - - // Close containing type declarations - for (int i = 1; i < typeDeclarations.Count; i++) + // Close all type declarations and namespace + while (writer.Indentation > 0) { writer.Indentation--; writer.WriteLine('}'); } - // Emit the file-scoped backing context class - writer.WriteLine(); - writer.WriteLine($"""[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{s_assemblyName.Name}", "{s_assemblyName.Version}")]"""); - writer.WriteLine($"[global::System.Text.Json.Serialization.JsonSerializable(typeof({fullyQualifiedTypeName}))]"); - writer.WriteLine($"file sealed partial class {pocoSpec.GeneratedContextName} : global::System.Text.Json.Serialization.JsonSerializerContext"); - writer.WriteLine('{'); - writer.WriteLine('}'); + AddSource($"{pocoSpec.TypeName}.JsonSerializable.g.cs", writer.ToSourceText()); + + // 2. Emit a separate file that provides the base class declaration for the synthetic context. + // The existing Emit() generates partial class files for the context but none of them + // declare the base class. For real user contexts, the user's source code provides this. + // For our synthetic context, we need this extra file. + var baseWriter = new SourceWriter(); + + baseWriter.WriteLine(""" + // + + #nullable enable annotations + #nullable disable warnings + + """); if (pocoSpec.Namespace != null) { - writer.Indentation--; - writer.WriteLine('}'); + baseWriter.WriteLine($"namespace {pocoSpec.Namespace}"); + baseWriter.WriteLine('{'); + baseWriter.Indentation++; } - AddSource($"{pocoSpec.TypeName}.JsonSerializable.g.cs", writer.ToSourceText()); + baseWriter.WriteLine($"internal sealed partial class {pocoSpec.GeneratedContextName} : global::System.Text.Json.Serialization.JsonSerializerContext"); + baseWriter.WriteLine('{'); + baseWriter.WriteLine('}'); + + if (pocoSpec.Namespace != null) + { + baseWriter.Indentation--; + baseWriter.WriteLine('}'); + } + + AddSource($"{pocoSpec.GeneratedContextName}.Base.g.cs", baseWriter.ToSourceText()); } private static SourceWriter CreateSourceWriterWithContextHeader(ContextGenerationSpec contextSpec, bool isPrimaryContextSourceFile = false, string? interfaceImplementation = null) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index c3f85d4a8c6b25..53c5156d54b479 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -214,8 +214,10 @@ private bool HasOnlyParameterlessJsonSerializableAttributes(INamedTypeSymbol typ /// /// Parses a type declaration annotated with the parameterless [JsonSerializable] attribute. + /// Returns a tuple of (PocoTypeGenerationSpec, ContextGenerationSpec) where the ContextGenerationSpec + /// is a synthetic context that can be emitted using the existing Emit() infrastructure. /// - public PocoTypeGenerationSpec? ParsePocoTypeGenerationSpec(TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken) + public (PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)? ParsePocoTypeGenerationSpec(TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken) { if (!_compilationContainsCoreJsonTypes) { @@ -307,18 +309,65 @@ private bool HasOnlyParameterlessJsonSerializableAttributes(INamedTypeSymbol typ string typeName = typeSymbol.Name; string generatedContextName = $"__JsonContext_{typeName}"; + string? ns = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } nsSymbol ? nsSymbol.ToDisplayString() : null; + string fqContextName = ns != null ? $"global::{ns}.{generatedContextName}" : $"global::{generatedContextName}"; - return new PocoTypeGenerationSpec + // Ensure context-scoped metadata caches are empty. + Debug.Assert(_typesToGenerate.Count == 0); + Debug.Assert(_generatedTypes.Count == 0); + + _contextClassLocation = location; + + // Enqueue the annotated type for type graph traversal + _typesToGenerate.Enqueue(new TypeToGenerate { - TypeRef = new(typeSymbol), + Type = _knownSymbols.Compilation.EraseCompileTimeMetadata(typeSymbol), + Mode = generationMode, + TypeInfoPropertyName = null, + Location = location, + AttributeLocation = parameterlessAttribute.GetLocation(), + }); + + // Walk the transitive type graph generating specs for every encountered type. + // Use the annotated type itself as the context type for accessibility checks. + while (_typesToGenerate.Count > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + TypeToGenerate typeToGenerate = _typesToGenerate.Dequeue(); + if (!_generatedTypes.ContainsKey(typeToGenerate.Type)) + { + TypeGenerationSpec spec = ParseTypeGenerationSpec(typeToGenerate, typeSymbol, options: null); + _generatedTypes.Add(typeToGenerate.Type, spec); + } + } + + var contextGenSpec = new ContextGenerationSpec + { + ContextType = new TypeRef(generatedContextName, fqContextName), + GeneratedTypes = _generatedTypes.Values.OrderBy(t => t.TypeRef.FullyQualifiedName).ToImmutableEquatableArray(), + Namespace = ns, + ContextClassDeclarations = new[] { $"internal sealed partial class {generatedContextName}" }.ToImmutableEquatableArray(), + GeneratedOptionsSpec = null, + }; + + var pocoSpec = new PocoTypeGenerationSpec + { + TypeRef = new TypeRef(typeSymbol), TypeName = typeName, TypeInfoPropertyName = propertyName, - Namespace = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, + Namespace = ns, TypeDeclarations = typeDeclarations.ToImmutableEquatableArray(), GeneratedContextName = generatedContextName, GenerationMode = generationMode, IsValueType = typeSymbol.IsValueType, }; + + // Clear the caches of generated metadata between the processing of types. + _generatedTypes.Clear(); + _typesToGenerate.Clear(); + _contextClassLocation = null; + + return (pocoSpec, contextGenSpec); } private static bool TryGetNestedTypeDeclarations(TypeDeclarationSyntax contextClassSyntax, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out List? typeDeclarations) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs index 934a94c40e8e91..e3feaeec3ae984 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs @@ -102,11 +102,14 @@ public void Execute(GeneratorExecutionContext executionContext) foreach ((TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel) in receiver.PocoTypeDeclarations) { - PocoTypeGenerationSpec? pocoSpec = parser.ParsePocoTypeGenerationSpec(typeDeclaration, semanticModel, executionContext.CancellationToken); - if (pocoSpec is not null) + (PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)? result = parser.ParsePocoTypeGenerationSpec(typeDeclaration, semanticModel, executionContext.CancellationToken); + if (result is not null) { Emitter emitter = new(executionContext); - emitter.EmitPocoType(pocoSpec); + // Emit the full backing context + emitter.Emit(result.Value.Context); + // Emit the static JsonTypeInfo property on the partial type + emitter.EmitPocoTypeProperty(result.Value.Poco); } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs index 9708ade2da8333..b2b9f391542df6 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs @@ -91,7 +91,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(diagnostics, EmitDiagnostics); // Pipeline 2: POCO-based source generation (parameterless [JsonSerializable] on data types) - IncrementalValuesProvider<(PocoTypeGenerationSpec?, ImmutableArray)> pocoGenerationSpecs = context.SyntaxProvider + IncrementalValuesProvider<((PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)?, ImmutableArray)> pocoGenerationSpecs = context.SyntaxProvider .ForAttributeWithMetadataName( #if !ROSLYN4_4_OR_GREATER context, @@ -109,9 +109,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { #pragma warning restore RS1035 Parser parser = new(tuple.Right); - PocoTypeGenerationSpec? pocoSpec = parser.ParsePocoTypeGenerationSpec(tuple.Left.TypeDeclaration, tuple.Left.SemanticModel, cancellationToken); + (PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)? result = parser.ParsePocoTypeGenerationSpec(tuple.Left.TypeDeclaration, tuple.Left.SemanticModel, cancellationToken); ImmutableArray pocoDiagnostics = parser.Diagnostics.ToImmutableArray(); - return (pocoSpec, pocoDiagnostics); + return (result, pocoDiagnostics); #pragma warning disable RS1035 } finally @@ -121,7 +121,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) #pragma warning restore RS1035 }); - IncrementalValuesProvider pocoSpecs = + IncrementalValuesProvider<(PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)?> pocoSpecs = pocoGenerationSpecs.Select(static (t, _) => t.Item1); context.RegisterSourceOutput(pocoSpecs, EmitPocoSource); @@ -167,9 +167,9 @@ private static void EmitDiagnostics(SourceProductionContext context, ImmutableAr } } - private static void EmitPocoSource(SourceProductionContext sourceProductionContext, PocoTypeGenerationSpec? pocoSpec) + private static void EmitPocoSource(SourceProductionContext sourceProductionContext, (PocoTypeGenerationSpec Poco, ContextGenerationSpec Context)? result) { - if (pocoSpec is null) + if (result is null) { return; } @@ -180,7 +180,10 @@ private static void EmitPocoSource(SourceProductionContext sourceProductionConte try { Emitter emitter = new(sourceProductionContext); - emitter.EmitPocoType(pocoSpec); + // Emit the full backing context using the existing infrastructure + emitter.Emit(result.Value.Context); + // Emit the static JsonTypeInfo property on the partial type + emitter.EmitPocoTypeProperty(result.Value.Poco); } finally { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index f28c4469971541..0368600388f642 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -1239,5 +1239,219 @@ internal partial class MyContext : JsonSerializerContext { } Compilation compilation = CompilationHelper.CreateCompilation(source); CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); } + + [Fact] + public void PocoJsonSerializable_GeneratesContextAndStaticProperty() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable] + public partial class WeatherForecast + { + public string City { get; set; } + public int Temperature { get; set; } + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + // Verify that the generated code compiles without errors + result.NewCompilation.GetDiagnostics().AssertMaxSeverity(DiagnosticSeverity.Info); + + // Verify no source generator diagnostics + Assert.Empty(result.Diagnostics); + + // Verify generated source files contain the expected content + var generatedTrees = result.NewCompilation.SyntaxTrees + .Where(t => t.FilePath.Contains("WeatherForecast.JsonSerializable.g.cs")) + .ToArray(); + Assert.Single(generatedTrees); + + string generatedCode = generatedTrees[0].GetText().ToString(); + Assert.Contains("partial class WeatherForecast", generatedCode); + Assert.Contains("JsonTypeInfo", generatedCode); + Assert.Contains("__JsonContext_WeatherForecast", generatedCode); + } + + [Fact] + public void PocoJsonSerializable_Struct_GeneratesContextAndStaticProperty() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable] + public partial struct Point + { + public int X { get; set; } + public int Y { get; set; } + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + result.NewCompilation.GetDiagnostics().AssertMaxSeverity(DiagnosticSeverity.Info); + Assert.Empty(result.Diagnostics); + + var generatedTrees = result.NewCompilation.SyntaxTrees + .Where(t => t.FilePath.Contains("Point.JsonSerializable.g.cs")) + .ToArray(); + Assert.Single(generatedTrees); + + string generatedCode = generatedTrees[0].GetText().ToString(); + Assert.Contains("partial struct Point", generatedCode); + Assert.Contains("JsonTypeInfo", generatedCode); + } + + [Fact] + public void PocoJsonSerializable_NonPartialType_EmitsDiagnostic() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable] + public class NotPartialClass + { + public string Name { get; set; } + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + // Expect SYSLIB1227 diagnostic + Assert.Contains(result.Diagnostics, d => d.Id == "SYSLIB1227"); + } + + [Fact] + public void PocoJsonSerializable_MemberNameCollision_EmitsDiagnostic() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable] + public partial class MyType + { + public string Name { get; set; } + public static int JsonTypeInfo => 42; + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + // Expect SYSLIB1228 diagnostic + Assert.Contains(result.Diagnostics, d => d.Id == "SYSLIB1228"); + } + + [Fact] + public void PocoJsonSerializable_CustomTypeInfoPropertyName_Works() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable(TypeInfoPropertyName = "MyTypeInfo")] + public partial class MyType + { + public string Name { get; set; } + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + result.NewCompilation.GetDiagnostics().AssertMaxSeverity(DiagnosticSeverity.Info); + Assert.Empty(result.Diagnostics); + + var generatedTrees = result.NewCompilation.SyntaxTrees + .Where(t => t.FilePath.Contains("MyType.JsonSerializable.g.cs")) + .ToArray(); + Assert.Single(generatedTrees); + + string generatedCode = generatedTrees[0].GetText().ToString(); + Assert.Contains("MyTypeInfo", generatedCode); + } + + [Fact] + public void PocoJsonSerializable_CoexistsWithExplicitContext() + { + string source = """ + using System.Text.Json.Serialization; + + namespace TestNamespace + { + [JsonSerializable] + public partial class WeatherForecast + { + public string City { get; set; } + public int Temperature { get; set; } + } + + public class OtherType + { + public string Value { get; set; } + } + + [JsonSerializable(typeof(OtherType))] + internal partial class ExplicitContext : JsonSerializerContext + { + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + result.NewCompilation.GetDiagnostics().AssertMaxSeverity(DiagnosticSeverity.Info); + Assert.Empty(result.Diagnostics); + + // Verify both the POCO-generated and the explicit context generated code exist + var pocoTrees = result.NewCompilation.SyntaxTrees + .Where(t => t.FilePath.Contains("WeatherForecast.JsonSerializable.g.cs")) + .ToArray(); + Assert.Single(pocoTrees); + + var contextTrees = result.NewCompilation.SyntaxTrees + .Where(t => t.FilePath.Contains("ExplicitContext.g.cs")) + .ToArray(); + Assert.Single(contextTrees); + } + + [Fact] + public void PocoJsonSerializable_GlobalNamespace_Works() + { + string source = """ + using System.Text.Json.Serialization; + + [JsonSerializable] + public partial class GlobalType + { + public string Name { get; set; } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger); + + result.NewCompilation.GetDiagnostics().AssertMaxSeverity(DiagnosticSeverity.Info); + Assert.Empty(result.Diagnostics); + } } } From bf56845a89c9a1129457d7362fc5f2e260919c4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:14:42 +0000 Subject: [PATCH 3/3] Add API compat suppressions for JsonSerializableAttribute AttributeUsage change Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8e75d756-a303-4334-8efc-7df66c802663 Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- .../src/CompatibilitySuppressions.xml | 21 +++++++++++++++++++ ...iCompatBaseline.NetCoreAppLatestStable.xml | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml b/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml index d1bee0f306888e..5db8750cb5e775 100644 --- a/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml +++ b/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml @@ -22,4 +22,25 @@ lib/netstandard2.0/System.Text.Json.dll true + + CP0015 + T:System.Text.Json.Serialization.JsonSerializableAttribute:[T:System.AttributeUsageAttribute] + lib/net10.0/System.Text.Json.dll + lib/net10.0/System.Text.Json.dll + true + + + CP0015 + T:System.Text.Json.Serialization.JsonSerializableAttribute:[T:System.AttributeUsageAttribute] + lib/net462/System.Text.Json.dll + lib/net462/System.Text.Json.dll + true + + + CP0015 + T:System.Text.Json.Serialization.JsonSerializableAttribute:[T:System.AttributeUsageAttribute] + lib/netstandard2.0/System.Text.Json.dll + lib/netstandard2.0/System.Text.Json.dll + true + diff --git a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml index 849c92f43630d9..c4d495739d0d6c 100644 --- a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml +++ b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml @@ -1027,4 +1027,10 @@ net10.0/System.Text.Json.dll net11.0/System.Text.Json.dll + + CP0015 + T:System.Text.Json.Serialization.JsonSerializableAttribute:[T:System.AttributeUsageAttribute] + net10.0/System.Text.Json.dll + net11.0/System.Text.Json.dll + \ No newline at end of file