Runtime: System.Text.Json serialization order

Created on 10 Dec 2019  路  18Comments  路  Source: dotnet/runtime

Serializer puts base properties at the end. In JSON net you could create a contract resolver that let you change serialization order.

class Entity
{
    public int Id { get; set; }
}

class Player : Entity
{
    public string Name { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(new Player {Id = 1, Name = "noname"}));
    }
}

the output will be:

{"Name":"noname","Id":1}

instead of

{"Id":1,"Name":"noname"} which is more readable

area-System.Text.Json enhancement

Most helpful comment

(Reprinted from issue #33854)

Currently, the serialization order is not guaranteed.
I use Json to save application settings, but there is a problem if the order is not guaranteed.

  • Diff cannot be obtained, making it difficult to find differences in settings.
  • Not suitable for version control.
  • The deserialization order is changed. -> The application initialization order is changed.

To fix the order, we propose the following enhancements:

  • Extend so that alphabetical sort flag or sort function (IComparer<PropertyInfo>) can be set in JsonSerializerOptions. This option only affects the output.

All 18 comments

In JSON net you could create a contract resolver that let you change serialization order.

Do you have a scenario in which you need to control the serialization order where the current behavior/order is blocking you? I am asking to understand the help motivate the feature since JSON is generally unordered (i.e. folks shouldn't be relying on the ordering of the properties within the payload).

{"Id":1,"Name":"noname"} which is more readable

Is that the primary benefit? Having the payload written in a specific order so it is easier to read? Adding support for such capabilities would be a bit of work, and this alone doesn't seem worth the trade off (at least when prioritizing features for 5.0).

The contract resolver feature in Newtonsoft.Json has some other capabilities that are currently not supported. How are you controlling the serialization order today (and is that the only capability you need)? Can you please share a code snippet of your resolver.

If ordering is super critical for you, a workaround would be to create and register a JsonConverter<Player>.

```C#
public class PlayerConverter : JsonConverter
{
public override Player Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

public override void Write(Utf8JsonWriter writer, Player value, JsonSerializerOptions options)
{
    writer.WriteStartObject();
    writer.WriteNumber(nameof(value.Id), value.Id);
    writer.WriteString(nameof(value.Name), value.Name);
    writer.WriteEndObject();
}

}

And here's how you can register and use it:
```C#
var options = new JsonSerializerOptions();
options.Converters.Add(new PlayerConverter());

Console.WriteLine(JsonSerializer.Serialize(new Player { Name = "noname", Id = 1 }, options));

order is very helpful when testing the API with tools like insomnia, postman etc. you instantly see what's the most important at the top, and naturally ID comes first and it's later easier to navigate in code because you know that base properties come first... JSON is meant to be human-readable otherwise I wouldn't use it. The ordering would be only helpful for the developer and many existing APIs put Id as the first JSON property. Why is it hard to do?

code for resolver:

public class BaseFirstContractResolver : DefaultContractResolver
{
    public BaseFirstContractResolver()
    {
        NamingStrategy = new CamelCaseNamingStrategy();
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        return base.CreateProperties(type, memberSerialization)
            .OrderBy(p => BaseTypesAndSelf(p.DeclaringType).Count()).ToList();

        IEnumerable<Type> BaseTypesAndSelf(Type t)
        {
            while (t != null)
            {
                yield return t;
                t = t.BaseType;
            }
        }
    }
}

no other benefits than readability, I think it would be more natural for base class properties to be serialized in the first place.

Have an unusual use case where we have one set of services in .NET for GETs and another set of services for write operations on another platform. We are doing ETag / If-Match checking by hashing JSON responses, so responses have to match across both platforms. It would be nice to be able to control this from the .NET side.

Also, if order really isn't important, could you consider putting the base properties first by default? This just seems to make more logical sense.

Hashing for cache invalidation/etag is also one I have come across. As it was a service to service scenario we moved to protobuf in the end to get the control over the order and bytes but that is not always possible for everyone

From @jogge in https://github.com/dotnet/runtime/issues/1085:

I'm missing an attribute for setting the order of the element in the JSON output.

Example:

I want the @SEGMENT to be the first element in the serialized JSON output of MyClass:

public abstract class MyBase
{
    [JsonPropertyName("@SEGMENT")]
    public virtual string Segment { get; set; } = "1";

    public int ID { get; set; }
}

public class MyClass : MyBase
{
    public string Name { get; set; }
}

The result when serializing MyClass is:

{
    "MyClass": {
      "Name": "Foo",
      "@SEGMENT": "1",
      "ID": 42
    }
}

I would like the serialized JSON output to be:

{
    "MyClass": {
      "@SEGMENT": "1",
      "Name": "Foo",
      "ID": 42
    }
}

or

{
    "MyClass": {
      "@SEGMENT": "1",
      "ID": 42,
      "Name": "Foo"
    }
}

Json.NET has an attribute for this: https://www.newtonsoft.com/json/help/html/JsonPropertyOrder.htm

Related question: .NET core 3: Order of serialization for JsonPropertyName (System.Text.Json.Serialization)

+1 for this ... very helpful for DX to be able to have key things at the top

(Reprinted from issue #33854)

Currently, the serialization order is not guaranteed.
I use Json to save application settings, but there is a problem if the order is not guaranteed.

  • Diff cannot be obtained, making it difficult to find differences in settings.
  • Not suitable for version control.
  • The deserialization order is changed. -> The application initialization order is changed.

To fix the order, we propose the following enhancements:

  • Extend so that alphabetical sort flag or sort function (IComparer<PropertyInfo>) can be set in JsonSerializerOptions. This option only affects the output.

Adding another use case for this feature. The GraphQL spec recommends here that the errors prop is rendered at the top, and indeed it's bitten me when debugging things to have it after data and not notice the errors as graphql is kind of unique in that it'll return data it resolved successfully and add errors for parts of the tree it couldn't resolve.

Open issue to implement this sorting is here. We'll be able to knock of the NewtonsoftJson serializer, ~but not the S.T.Json one until this is possible.~ and I've just realised for S.T.Json we are using a custom converter to write the response, so can just shift the property write ordering in there without need for this feature for S.T.Json. Still, there's another example use case I guess.

Also Algorand's canonical encoding is a MessagePack with sorted json properties. Seems more straightforward to do this with NewtonsoftJson at the moment.

This would be a very useful feature to have. I've been using System.Text.Json for handling "huge" amounts of metadata that get checked into source control for about 8 months now, and until today the order has always been the same. I had assumed it was guaranteed all along.

To add another use case for controlling serialization order, Safe Exam Browser requires generating a sorted JSON object for their Config Key algorithm since it involves comparing hashes.

Btw. this is the code responsible for the behaviour:

https://github.com/dotnet/runtime/blob/650f28f2c841c44b0a247e18a9f983c8619286dc/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs#L111-L143

Maybe Stack<T> could be used to push base types and then just pop and get properties in the appropriate order.

var typeStack = new Stack<Type>();
for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
{
    typeStack.Push(currentType);
}

Whatever will result in the best performance... maybe sorting by MetadataToken ? https://stackoverflow.com/questions/8067493/if-getfields-doesnt-guarantee-order-how-does-layoutkind-sequential-work

Should this be a default behaviour? Should this be configurable? I can confirm that sorting by MetadataToken works fine.

@layomia
Here is a proposed implementation/prototype of JsonProperty ordering

It introduces 2 new attributes: [JsonPropertyOrder] and [JsonPropertyOrderByName]

JsonPropertyOrder
Property or field Attribute to specify the order of the given property (compatible with the Newtonsoft JSON JsonProperty.Order behaviour).

JsonPropertyOrderByName
Class Attribute. If it is specified, the properties will be sorted by name (using an optional StringComparison arg)

Sorting logic
Calling sorting
Sorting function

Examples and expected serialization

        public struct PropertyOrderTestStruct1
        {
            [JsonPropertyOrder(1)]
            public int A { get; set; }
            public int B { get; set; }
            [JsonPropertyOrder(-2)]
            public int C { get; set; }

            public static readonly string e_json = @"{""C"":0,""B"":0,""A"":0}";
        }

        public class PropertyOrderTestClass1
        {
            [JsonPropertyOrder(-1)]
            public int A { get; set; } = 1;
            [JsonPropertyOrder(-1)]
            public int B { get; set; } = 2;
            [JsonPropertyOrder(-1)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""A"":1,""B"":2,""C"":3}";
        }

        public class PropertyOrderTestClass2
        {
            [JsonPropertyOrder(2)]
            public int a { get; set; } = 1;
            [JsonPropertyOrder(1)]
            public int b { get; set; } = 2;
            [JsonPropertyOrder(0)]
            public int c { get; set; } = 3;

            public static readonly string e_json = @"{""c"":3,""b"":2,""a"":1}";
        }

        public class PropertyOrderTestClass3
        {
            public int A { get; set; } = 1;
            public int B { get; set; } = 2;
            [JsonPropertyOrder(-2)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""C"":3,""A"":1,""B"":2}";
        }

        public class PropertyOrderTestClass4
        {
            [JsonPropertyOrder(1)]
            public int A { get; set; } = 1;
            [JsonPropertyOrder(2)]
            public int B { get; set; } = 2;
            [JsonPropertyOrder(-1)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""C"":3,""A"":1,""B"":2}";
        }

        public class PropertyOrderTestClass5
        {
            [JsonPropertyOrder(2)]
            public int A { get; set; } = 1;
            [JsonPropertyOrder(-1)]
            public int B { get; set; } = 2;
            [JsonPropertyOrder(1)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""B"":2,""C"":3,""A"":1}";
        }

        public class PropertyOrderTestClass6
        {
            [JsonPropertyOrder(0)]
            public int A { get; set; } = 1;
            [JsonPropertyOrder(0)]
            public int B { get; set; } = 2;
            [JsonPropertyOrder(0)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""A"":1,""B"":2,""C"":3}";
        }

        public class PropertyOrderTestClass7
        {
            [JsonPropertyOrder(1)]
            public int A { get; set; } = 1;
            public int B { get; set; } = 2;
            [JsonPropertyOrder(-2)]
            public int C { get; set; } = 3;

            public static readonly string e_json = @"{""C"":3,""B"":2,""A"":1}";
        }

        [JsonPropertyOrderByName]
        public class PropertyOrderTestClass8
        {
            public int C { get; set; } = 3;
            public int B { get; set; } = 2;
            public int A { get; set; } = 1;

            public static readonly string e_json = @"{""A"":1,""B"":2,""C"":3}";
        }

        [JsonPropertyOrderByName()]
        public class PropertyOrderTestClass9
        {
            public int cC { get; set; } = 3;
            public int CC { get; set; } = 3;
            public int bB { get; set; } = 2;
            public int BB { get; set; } = 2;
            public int aA { get; set; } = 1;
            public int AA { get; set; } = 1;

            public static readonly string e_json = @"{""AA"":1,""BB"":2,""CC"":3,""aA"":1,""bB"":2,""cC"":3}";
        }

        [JsonPropertyOrderByName(StringComparison.OrdinalIgnoreCase)]
        public class PropertyOrderTestClass10
        {
            public int cC { get; set; } = 3;
            public int CC { get; set; } = 3;
            public int bB { get; set; } = 2;
            public int BB { get; set; } = 2;
            public int aA { get; set; } = 1;
            public int AA { get; set; } = 1;

            public static readonly string e_json = @"{""aA"":1,""AA"":1,""bB"":2,""BB"":2,""cC"":3,""CC"":3}";
        }

        [JsonPropertyOrderByName]
        public class PropertyOrderTestClass11
        {
            [JsonPropertyName("C")]
            public int a { get; set; } = 3;
            [JsonPropertyName("B")]
            public int b { get; set; } = 2;
            [JsonPropertyName("A")]
            public int c { get; set; } = 1;

            public static readonly string e_json = @"{""A"":1,""B"":2,""C"":3}";
        }

        [JsonPropertyOrderByName]
        public class PropertyOrderTestClass12
        {
            [JsonPropertyName("C")]
            [JsonPropertyOrder(-2)]
            public int a { get; set; } = 3;
            [JsonPropertyName("B")]
            public int b { get; set; } = 2;
            [JsonPropertyName("A")]
            public int c { get; set; } = 1;

            public static readonly string e_json = @"{""C"":3,""A"":1,""B"":2}";
        }

Looks promising!

Would it make sense to also have a simple JsonSerializerOptions setting that does the same thing as JsonPropertyOrderByName, for the whole structure being serialized, or am I the only one interested in such an option?

Would it make sense to also have a simple JsonSerializerOptions setting that does the same thing as JsonPropertyOrderByName, for the whole structure being serialized, or am I the only one interested in such an option?

The implementation should be fairly trivial. It is more of a design/vision question.

In JSON net you could create a contract resolver that let you change serialization order.

Do you have a scenario in which you need to control the serialization order where the current behavior/order is blocking you? I am asking to understand the help motivate the feature since JSON is generally unordered (i.e. folks shouldn't be relying on the ordering of the properties within the payload).

{"Id":1,"Name":"noname"} which is more readable

Is that the primary benefit? Having the payload written in a specific order so it is easier to read? Adding support for such capabilities would be a bit of work, and this alone doesn't seem worth the trade off (at least when prioritizing features for 5.0).

The contract resolver feature in Newtonsoft.Json has some other capabilities that are currently not supported. How are you controlling the serialization order today (and is that the only capability you need)? Can you please share a code snippet of your resolver.

If ordering is super critical for you, a workaround would be to create and register a JsonConverter<Player>.

public class PlayerConverter : JsonConverter<Player>
{
    public override Player Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Player value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WriteNumber(nameof(value.Id), value.Id);
        writer.WriteString(nameof(value.Name), value.Name);
        writer.WriteEndObject();
    }
}

And here's how you can register and use it:

var options = new JsonSerializerOptions();
options.Converters.Add(new PlayerConverter());

Console.WriteLine(JsonSerializer.Serialize(new Player { Name = "noname", Id = 1 }, options));

@ahsonkhan, I even agree that people don't care about the order of JSON properties. But, what about when you use the JSON received in the request, to convert it into XML, which in turn, must have a specific ordering?
Note: This is a real use case, and it kept me using Newtonsoft ([JsonProperty (Order = xx)])

Another use-case: iterate through a response array on the client-side using the object's properties to create an html table (and the headings). Useful when a lot of stuff (including the client component) is generic, to have this as a known convention.

I use Json to save application settings, but there is a problem if the order is not guaranteed.

This can be fixed by using XML for config files. Which for most things .net should preserve ordering (preserving formatting may require a little extra work ). Also you get the ability to comment and explain what the settings are for and what eligible values are, which is a win for dev and support teams.

I even agree that people don't care about the order of JSON properties

I care, and we all should care. Not adhering to an order makes supporting the system more difficult and time consuming. There is an order being chosen, and that choice needs to be reliable.

what about when you use the JSON received in the request, to convert it into XML, which in turn, must have a specific ordering?

@silvairsoares - I agree, and this idea extends much farther than just the json->xml. Any machine generated data that may need to be inspected (compared, etc.) or should have a guaranteed and well known ordering. This could be config objects turned into config files, or the result of code generation from an API, .*proj/.sln files, data payloads exchanged between systems, and so on.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

GitAntoinee picture GitAntoinee  路  3Comments

matty-hall picture matty-hall  路  3Comments

jkotas picture jkotas  路  3Comments

bencz picture bencz  路  3Comments

btecu picture btecu  路  3Comments