Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

While migrating to .NET Core 3 I've switched from Newtonsoft.Json serialization to System.Text.Json.Serialization. Of all the features I want to continue using JsonPropertyName attribute.

Newtonsoft version allowed ordering of serialized attributes:

[JsonProperty(Order = 1)]
public bool Deleted { get; set; }

[JsonProperty(Order = 2)]
public DateTime DeletedDate { get; set; }

Is there a way to achieve the same in System.Text.Json.Serialization?

like image 503
Sergey Nikitin Avatar asked Dec 02 '19 07:12

Sergey Nikitin


People also ask

What is JSON property order?

The @JsonPropertyOrder is an annotation to be used at the class-level. It takes as property a list of fields that defines the order in which fields can appear in the string resulting from the object JSON serialization.

What is the difference between System text JSON and Newtonsoft JSON?

System. Text. Json focuses primarily on performance, security, and standards compliance. It has some key differences in default behavior and doesn't aim to have feature parity with Newtonsoft.

Is polymorphic deserialization possible in System text JSON?

NET 7, System. Text. Json supports polymorphic type hierarchy serialization and deserialization with attribute annotations.

Which one is correct class for JSON serializer?

The JsonSerializer is a static class in the System. Text. Json namespace. It provides functionality for serializing objects to a JSON string and deserializing from a JSON string to objects.


2 Answers

It's supported in .Net 6 and greater using JsonPropertyOrderAttribute:

JsonPropertyOrderAttribute Class

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.

The attribute can be applied e.g. as follows:

[JsonPropertyOrder(order : 1)]
like image 104
Murilo Maciel Curti Avatar answered Sep 20 '22 14:09

Murilo Maciel Curti


While this feature is not implemented in .NET Core, we can apply desired ordering by creating a custom JsonConverter. There are a few ways how that can be achievable. Below is the implementation I've came up with.

Explanation - the JsonPropertyOrderConverter handles the types having at least one property with a custom order value applied. For each of those types, it creates and caches a sorter function that converts an original object into an ExpandoObject with the properties set in a specific order. ExpandoObject maintains the order of properties, so it can be passed back to JsonSerializer for further serialization. The converter also respects JsonPropertyNameAttribute and JsonPropertyOrderAttribute attributes applied to serializing properties.

Please note that Sorter functions deal with PropertyInfo objects that can add some extra latency. If the performance is critical in your scenario, consider implementing Function<object, object> sorter based on Expression trees.

class Program
{
    static void Main(string[] args)
    {
        var test = new Test { Bar = 1, Baz = 2, Foo = 3 };

        // Add JsonPropertyOrderConverter to enable ordering
        var opts = new JsonSerializerOptions();
        opts.Converters.Add(new JsonPropertyOrderConverter());

        var serialized = JsonSerializer.Serialize(test, opts);

        // Outputs: {"Bar":1,"Baz":2,"Foo":3}
        Console.WriteLine(serialized);
    }
}

class Test
{
    [JsonPropertyOrder(1)]
    public int Foo { get; set; }

    [JsonPropertyOrder(-1)]
    public int Bar { get; set; }

    // Default order is 0
    public int Baz { get; set; }

}

/// <summary>
/// Sets a custom serialization order for a property.
/// The default value is 0.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
sealed class JsonPropertyOrderAttribute : Attribute
{
    public int Order { get; }

    public JsonPropertyOrderAttribute(int order)
    {
        Order = order;
    }
}

/// <summary>
/// For Serialization only.
/// Emits properties in the specified order.
/// </summary>
class JsonPropertyOrderConverter : JsonConverter<object>
{
    delegate ExpandoObject SorterFunc(object value, bool ignoreNullValues);

    private static readonly ConcurrentDictionary<Type, SorterFunc> _sorters
        = new ConcurrentDictionary<Type, SorterFunc>();

    public override bool CanConvert(Type typeToConvert)
    {
        // Converter will not run if there is no custom order applied
        var sorter = _sorters.GetOrAdd(typeToConvert, CreateSorter);
        return sorter != null;
    }

    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        // Resolve the sorter.
        // It must exist here (see CanConvert).
        var sorter = _sorters.GetOrAdd(value.GetType(), CreateSorter);

        // Convert value to an ExpandoObject
        // with a certain property order
        var sortedValue = sorter(value, options.IgnoreNullValues);

        // Serialize the ExpandoObject
        JsonSerializer.Serialize(writer, (IDictionary<string, object>)sortedValue, options);
    }

    private SorterFunc CreateSorter(Type type)
    {
        // Get type properties ordered according to JsonPropertyOrder value
        var sortedProperties = type
            .GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(x => x.GetCustomAttribute<JsonIgnoreAttribute>(true) == null)
            .Select(x => new
            {
                Info = x,
                Name = x.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name ?? x.Name,
                Order = x.GetCustomAttribute<JsonPropertyOrderAttribute>(true)?.Order ?? 0,
                IsExtensionData = x.GetCustomAttribute<JsonExtensionDataAttribute>(true) != null
            })
            .OrderBy(x => x.Order)
            .ToList();

        // If all properties have the same order,
        // there is no sense in explicit sorting
        if (!sortedProperties.Any(x => x.Order != 0))
        {
            return null;
        }
        
        // Return a function assigning property values
        // to an ExpandoObject in a specified order
        return new SorterFunc((src, ignoreNullValues) =>
        {
            IDictionary<string, object> dst = new ExpandoObject();
           
            var isExtensionDataProcessed = false;

            foreach (var prop in sortedProperties)
            {
                var propValue = prop.Info.GetValue(src);

                if (prop.IsExtensionData)
                {
                    if (propValue is IDictionary extensionData)
                    {
                        if (isExtensionDataProcessed)
                        {
                            throw new InvalidOperationException($"The type '{src.GetType().FullName}' cannot have more than one property that has the attribute '{typeof(JsonExtensionDataAttribute).FullName}'.");
                        }

                        foreach (DictionaryEntry entry in extensionData)
                        {
                            dst.Add((string)entry.Key, entry.Value);
                        }
                    }
                    
                    isExtensionDataProcessed = true;
                }
                else if (!ignoreNullValues || !(propValue is null))
                {
                    dst.Add(prop.Name, propValue);
                }
            }

            return (ExpandoObject)dst;
        });
    }
}
like image 25
AndreyCh Avatar answered Sep 19 '22 14:09

AndreyCh