diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index cefb9de2d7164..727d80c6b1084 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -34,6 +34,8 @@ private sealed class Parser private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute"; + private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute"; + private readonly GeneratorExecutionContext _executionContext; private readonly MetadataLoadContextInternal _metadataLoadContext; @@ -606,6 +608,8 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + bool propertyOrderSpecified = false; + for (Type? currentType = type; currentType != null; currentType = currentType.BaseType) { foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags)) @@ -620,6 +624,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener PropertyGenerationSpec spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode); CacheMember(spec, ref propGenSpecList, ref ignoredMembers); + propertyOrderSpecified |= spec.Order != 0; } foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) @@ -631,8 +636,14 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener PropertyGenerationSpec spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode); CacheMember(spec, ref propGenSpecList, ref ignoredMembers); + propertyOrderSpecified |= spec.Order != 0; } } + + if (propertyOrderSpecified) + { + propGenSpecList.Sort((p1, p2) => p1.Order.CompareTo(p2.Order)); + } } typeMetadata.Initialize( @@ -697,6 +708,7 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, bool foundDesignTimeCustomConverter = false; string? converterInstantiationLogic = null; + int order = 0; foreach (CustomAttributeData attributeData in attributeDataList) { @@ -745,6 +757,12 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, // Null check here is done at runtime within JsonSerializer. } break; + case JsonPropertyOrderAttributeFullName: + { + IList ctorArgs = attributeData.ConstructorArguments; + order = (int)ctorArgs[0].Value; + } + break; default: break; } @@ -839,6 +857,7 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, SetterIsVirtual = setterIsVirtual, DefaultIgnoreCondition = ignoreCondition, NumberHandling = numberHandling, + Order = order, HasJsonInclude = hasJsonInclude, TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode), DeclaringTypeRef = $"global::{memberInfo.DeclaringType.GetUniqueCompilableTypeName()}", diff --git a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs index 5d52ae2e3553e..9d05fdf297cdd 100644 --- a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs @@ -63,6 +63,11 @@ internal sealed class PropertyGenerationSpec /// public JsonNumberHandling? NumberHandling { get; init; } + /// + /// The serialization order of the property. + /// + public int Order { get; init; } + /// /// Whether the property has the JsonIncludeAttribute. If so, non-public accessors can be used for (de)serialziation. /// diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index d07a96cd6f2b2..c4ade896e1d64 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -20,6 +20,7 @@ public interface ITestContext public JsonTypeInfo MyType { get; } public JsonTypeInfo MyType2 { get; } public JsonTypeInfo MyTypeWithCallbacks { get; } + public JsonTypeInfo MyTypeWithPropertyOrdering { get; } public JsonTypeInfo MyIntermediateType { get; } public JsonTypeInfo HighLowTempsImmutable { get; } public JsonTypeInfo MyNestedClass { get; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs index 3c67d58bfd327..94f77f5bc0cd4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -18,6 +18,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] [JsonSerializable(typeof(MyTypeWithCallbacks))] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -47,6 +48,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MetadataAndSerializationContext.Default.MyType.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyType2.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyTypeWithCallbacks.Serialize); + Assert.NotNull(MetadataAndSerializationContext.Default.MyTypeWithPropertyOrdering.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyIntermediateType.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs index 0b253d2ec64f9..101a53e02246a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -17,6 +17,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -67,6 +68,7 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] [JsonSerializable(typeof(MyTypeWithCallbacks))] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -96,6 +98,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.MyType.Serialize); Assert.Null(MetadataContext.Default.MyType2.Serialize); Assert.Null(MetadataContext.Default.MyTypeWithCallbacks.Serialize); + Assert.Null(MetadataContext.Default.MyTypeWithPropertyOrdering.Serialize); Assert.Null(MetadataContext.Default.MyIntermediateType.Serialize); Assert.Null(MetadataContext.Default.HighLowTempsImmutable.Serialize); Assert.Null(MetadataContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index b4f64091ca578..aaf1f17fc2224 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -17,6 +17,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Default)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -45,6 +46,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MixedModeContext.Default.MyType.Serialize); Assert.NotNull(MixedModeContext.Default.MyType2.Serialize); Assert.NotNull(MixedModeContext.Default.MyTypeWithCallbacks.Serialize); + Assert.NotNull(MixedModeContext.Default.MyTypeWithPropertyOrdering.Serialize); Assert.NotNull(MixedModeContext.Default.MyIntermediateType.Serialize); Assert.Null(MixedModeContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index 83902a98966c7..0017021fb4edd 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -561,5 +561,13 @@ protected static void AssertFastPathLogicCorrect(string expectedJson, T value JsonTestHelper.AssertJsonEqual(expectedJson, Encoding.UTF8.GetString(ms.ToArray())); } + + [Fact] + public void PropertyOrdering() + { + MyTypeWithPropertyOrdering obj = new(); + string json = JsonSerializer.Serialize(obj, DefaultContext.MyTypeWithPropertyOrdering); + Assert.Equal("{\"C\":0,\"B\":0,\"A\":0}", json); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index e58856ee97ae9..0fb3c4e9ddef8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -18,6 +18,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(MyType2))] [JsonSerializable(typeof(MyTypeWithCallbacks))] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] @@ -40,6 +41,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -63,6 +65,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(MyType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyType2), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyTypeWithCallbacks), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -97,6 +100,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationContext.Default.MyType.Serialize); Assert.NotNull(SerializationContext.Default.MyType2.Serialize); Assert.NotNull(SerializationContext.Default.MyTypeWithCallbacks.Serialize); + Assert.NotNull(SerializationContext.Default.MyTypeWithPropertyOrdering.Serialize); Assert.NotNull(SerializationContext.Default.MyIntermediateType.Serialize); Assert.NotNull(SerializationContext.Default.HighLowTempsImmutable.Serialize); Assert.NotNull(SerializationContext.Default.MyNestedClass.Serialize); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index e43e1bb2b9e13..ff110fe377933 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -117,6 +117,18 @@ public class MyTypeWithCallbacks : IJsonOnSerializing, IJsonOnSerialized void IJsonOnSerialized.OnSerialized() => MyProperty = "After"; } + public class MyTypeWithPropertyOrdering + { + public int B { get; set; } + + [JsonPropertyOrder(1)] + public int A { get; set; } + + [JsonPropertyOrder(-1)] + [JsonInclude] + public int C = 0; + } + public class JsonMessage { public string Message { get; set; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyOrderTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyOrderTests.cs index 07b16caa00dcd..c5f4d73a9b613 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyOrderTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyOrderTests.cs @@ -15,7 +15,8 @@ private class MyPoco_BeforeAndAfter public int A { get; set; } [JsonPropertyOrder(-1)] - public int C { get; set; } + [JsonInclude] + public int C = 0; } [Fact] @@ -28,7 +29,8 @@ public static void BeforeAndAfterDefaultOrder() private class MyPoco_After { [JsonPropertyOrder(2)] - public int C { get; set; } + [JsonInclude] + public int C = 0; public int B { get; set; } public int D { get; set; } @@ -48,7 +50,8 @@ public static void AfterDefaultOrder() private class MyPoco_Before { [JsonPropertyOrder(-1)] - public int C { get; set; } + [JsonInclude] + public int C = 0; public int B { get; set; } public int D { get; set; }