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 26b72dd6493aa..97e39311a09e7 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -832,6 +832,12 @@ public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Seriali public JsonPropertyNameAttribute(string name) { } public string Name { get { throw null; } } } + [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)] + public sealed partial class JsonPropertyOrderAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonPropertyOrderAttribute(int order) { } + public int Order { get { throw null; } } + } [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true)] public sealed partial class JsonSerializableAttribute : System.Text.Json.Serialization.JsonAttribute { diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 3206cbd6de1f6..08855731abe63 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -22,7 +22,7 @@ - + @@ -89,6 +89,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyOrderAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyOrderAttribute.cs new file mode 100644 index 0000000000000..8126e75690f41 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyOrderAttribute.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Specifies the property order that is present in the JSON when serializing. Lower values are serialized first. + /// If the attribute is not specified, the default value is 0. + /// + /// If multiple properties have the same value, the ordering is undefined between them. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + public sealed class JsonPropertyOrderAttribute : JsonAttribute + { + /// + /// Initializes a new instance of with the specified order. + /// + /// The order of the property. + public JsonPropertyOrderAttribute(int order) + { + Order = order; + } + + /// + /// The serialization order of the property. + /// + public int Order { get; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 95081b8519dea..9e34cc60a3561 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -74,6 +74,12 @@ internal virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, JsonNumb DeterminePropertyName(); DetermineIgnoreCondition(ignoreCondition); + JsonPropertyOrderAttribute? orderAttr = GetAttribute(MemberInfo); + if (orderAttr != null) + { + Order = orderAttr.Order; + } + JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); DetermineNumberHandlingForProperty(attribute?.Handling, declaringTypeNumberHandling); } @@ -366,6 +372,11 @@ internal abstract void InitializeForTypeInfo( internal JsonSerializerOptions Options { get; set; } = null!; // initialized in Init method + /// + /// The property order. + /// + internal int Order { get; set; } + internal bool ReadJsonAndAddExtensionProperty( object obj, ref ReadStack state, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index ba90400e24650..077de6d05372d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -197,6 +197,8 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json PropertyInfo[] properties = type.GetProperties(bindingFlags); + bool propertyOrderSpecified = false; + // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance // is finished initializing and added to the cache on JsonSerializerOptions. // Default 'capacity' to the common non-polymorphic + property case. @@ -229,6 +231,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json propertyInfo, isVirtual, typeNumberHandling, + ref propertyOrderSpecified, ref ignoredMembers); } else @@ -263,6 +266,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json fieldInfo, isVirtual: false, typeNumberHandling, + ref propertyOrderSpecified, ref ignoredMembers); } } @@ -286,6 +290,11 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json properties = currentType.GetProperties(bindingFlags); }; + if (propertyOrderSpecified) + { + PropertyCache.List.Sort((p1, p2) => p1.Value!.Order.CompareTo(p2.Value!.Order)); + } + if (converter.ConstructorIsParameterized) { InitializeConstructorParameters(converter.ConstructorInfo!); @@ -327,6 +336,7 @@ private void CacheMember( MemberInfo memberInfo, bool isVirtual, JsonNumberHandling? typeNumberHandling, + ref bool propertyOrderSpecified, ref Dictionary? ignoredMembers) { bool hasExtensionAttribute = memberInfo.GetCustomAttribute(typeof(JsonExtensionDataAttribute)) != null; @@ -347,6 +357,7 @@ private void CacheMember( else { CacheMember(jsonPropertyInfo, PropertyCache, ref ignoredMembers); + propertyOrderSpecified |= jsonPropertyInfo.Order != 0; } } 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 new file mode 100644 index 0000000000000..07b16caa00dcd --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyOrderTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static class PropertyOrderTests + { + private class MyPoco_BeforeAndAfter + { + public int B { get; set; } + + [JsonPropertyOrder(1)] + public int A { get; set; } + + [JsonPropertyOrder(-1)] + public int C { get; set; } + } + + [Fact] + public static void BeforeAndAfterDefaultOrder() + { + string json = JsonSerializer.Serialize(new MyPoco_BeforeAndAfter()); + Assert.Equal("{\"C\":0,\"B\":0,\"A\":0}", json); + } + + private class MyPoco_After + { + [JsonPropertyOrder(2)] + public int C { get; set; } + + public int B { get; set; } + public int D { get; set; } + + [JsonPropertyOrder(1)] + public int A { get; set; } + } + + [Fact] + public static void AfterDefaultOrder() + { + string json = JsonSerializer.Serialize(new MyPoco_After()); + Assert.EndsWith("\"A\":0,\"C\":0}", json); + // Order of B and D are not defined except they come before A and C + } + + private class MyPoco_Before + { + [JsonPropertyOrder(-1)] + public int C { get; set; } + + public int B { get; set; } + public int D { get; set; } + + [JsonPropertyOrder(-2)] + public int A { get; set; } + } + + [Fact] + public static void BeforeDefaultOrder() + { + string json = JsonSerializer.Serialize(new MyPoco_Before()); + Assert.StartsWith("{\"A\":0,\"C\":0", json); + // Order of B and D are not defined except they come after A and C + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 20d919ca164e2..9e117638ddb80 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -166,6 +166,7 @@ +