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 3fd7785a1b27a9..965bf7fc210751 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn3.11.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Globalization;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -43,45 +44,58 @@ public void Initialize(GeneratorInitializationContext context)
///
public void Execute(GeneratorExecutionContext executionContext)
{
- if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.ContextClassDeclarations == null)
+ // Ensure the source generator parses and emits using invariant culture.
+ // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI)
+ // from being written to generated source files.
+ // Note: RS1035 is already disabled at the file level for this Roslyn version.
+ CultureInfo originalCulture = CultureInfo.CurrentCulture;
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ try
{
- // nothing to do yet
- return;
- }
+ if (executionContext.SyntaxContextReceiver is not SyntaxContextReceiver receiver || receiver.ContextClassDeclarations == null)
+ {
+ // 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);
+ // 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)
- {
- ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(contextClassDeclaration, semanticModel, executionContext.CancellationToken);
- if (contextGenerationSpec is null)
+ 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);
- }
+ // Stage 2. Report any diagnostics gathered by the parser.
+ foreach (DiagnosticInfo diagnosticInfo in parser.Diagnostics)
+ {
+ executionContext.ReportDiagnostic(diagnosticInfo.CreateDiagnostic());
+ }
- // Stage 2. Report any diagnostics gathered by the parser.
- foreach (DiagnosticInfo diagnosticInfo in parser.Diagnostics)
- {
- executionContext.ReportDiagnostic(diagnosticInfo.CreateDiagnostic());
- }
+ if (contextGenerationSpecs is null)
+ {
+ return;
+ }
- if (contextGenerationSpecs is null)
- {
- return;
+ // 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);
+ }
}
-
- // Stage 3. Emit source code from the spec models.
- OnSourceEmitting?.Invoke(contextGenerationSpecs.ToImmutableArray());
- Emitter emitter = new(executionContext);
- foreach (ContextGenerationSpec contextGenerationSpec in contextGenerationSpecs)
+ finally
{
- emitter.Emit(contextGenerationSpec);
+ CultureInfo.CurrentCulture = originalCulture;
}
}
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 447f54c7f07821..75f1f51269e7a6 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
+using System.Globalization;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -42,10 +43,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Combine(knownTypeSymbols)
.Select(static (tuple, cancellationToken) =>
{
- Parser parser = new(tuple.Right);
- ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(tuple.Left.ContextClass, tuple.Left.SemanticModel, cancellationToken);
- ImmutableEquatableArray diagnostics = parser.Diagnostics.ToImmutableEquatableArray();
- return (contextGenerationSpec, diagnostics);
+ // Ensure the source generator parses using invariant culture.
+ // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI)
+ // from being written to generated source files.
+#pragma warning disable RS1035 // CultureInfo.CurrentCulture is banned in analyzers
+ CultureInfo originalCulture = CultureInfo.CurrentCulture;
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ try
+ {
+#pragma warning restore RS1035
+ Parser parser = new(tuple.Right);
+ ContextGenerationSpec? contextGenerationSpec = parser.ParseContextGenerationSpec(tuple.Left.ContextClass, tuple.Left.SemanticModel, cancellationToken);
+ ImmutableEquatableArray diagnostics = parser.Diagnostics.ToImmutableEquatableArray();
+ return (contextGenerationSpec, diagnostics);
+#pragma warning disable RS1035
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+#pragma warning restore RS1035
})
#if ROSLYN4_4_OR_GREATER
.WithTrackingName(SourceGenerationSpecTrackingName)
@@ -69,8 +86,23 @@ private void ReportDiagnosticsAndEmitSource(SourceProductionContext sourceProduc
}
OnSourceEmitting?.Invoke(ImmutableArray.Create(input.ContextGenerationSpec));
- Emitter emitter = new(sourceProductionContext);
- emitter.Emit(input.ContextGenerationSpec);
+
+ // Ensure the source generator emits number literals using invariant culture.
+ // This prevents issues such as locale-specific negative signs (e.g., U+2212 in fi-FI)
+ // from being written to generated source files.
+#pragma warning disable RS1035 // CultureInfo.CurrentCulture is banned in analyzers
+ CultureInfo originalCulture = CultureInfo.CurrentCulture;
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ try
+ {
+ Emitter emitter = new(sourceProductionContext);
+ emitter.Emit(input.ContextGenerationSpec);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+#pragma warning restore RS1035
}
///
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 9faf0472700dd5..ba940f04a2fe00 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
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Linq;
+using System.Tests;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
@@ -965,5 +966,51 @@ public partial class MyContext : JsonSerializerContext
Assert.NotEmpty(result.NewCompilation.GetDiagnostics().Where(d => d.Id == "EXP001"));
}
#endif
+
+ [Fact]
+ public void NegativeJsonPropertyOrderGeneratesValidCode()
+ {
+ // Test for https://github.com/dotnet/runtime/issues/121277
+ // Verify that negative JsonPropertyOrder values generate compilable code
+ // even on locales that use non-ASCII minus signs (e.g., fi_FI uses U+2212)
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace Test
+ {
+ public class MyClass
+ {
+ [JsonPropertyOrder(-1)]
+ public int FirstProperty { get; set; }
+
+ [JsonPropertyOrder(0)]
+ public int SecondProperty { get; set; }
+
+ [JsonPropertyOrder(-100)]
+ public int ThirdProperty { get; set; }
+ }
+
+ [JsonSerializable(typeof(MyClass))]
+ public partial class MyContext : JsonSerializerContext
+ {
+ }
+ }
+ """;
+
+ // Test with fi_FI culture which uses U+2212 minus sign for negative numbers
+ using (new ThreadCultureChange("fi-FI"))
+ {
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger);
+
+ // The generated code should compile without errors
+ // If the bug exists, we'd see CS1525, CS1002, CS1056, or CS0201 errors
+ var errors = result.NewCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+
+ Assert.Empty(errors);
+ }
+ }
}
}