steeveeet
steeveeet

Reputation: 668

json.net: serialise base class members first

I'm using json.net to store a serialised object that I would like people to be able to edit in a text editor. I have a base class that contains the name of the object and then a class that inherits from that and adds some other properties.

The problem is that the the properties are written out such that the derived classes properties are written first, and then the base class afetr, so I get:

{
  "MySpecialFiled": 4,
  "Name": "This Is My Object",
  "AnotherBaseField": 8,
}

rather than:

{
  "Name": "This Is My Object",
  "AnotherBaseField": 8,
  "MySpecialFiled": 4,
}

You can see how this get's a bit of a pain when you have a bunch of fields in the derived class and want to actually view/edit in a text editor!

I've messed around with the source code particularly:

public static IEnumerable<FieldInfo> GetFields(Type targetType, BindingFlags bindingAttr)

and

public static IEnumerable<PropertyInfo> GetProperties(Type targetType, BindingFlags bindingAttr)

in ReflectionUtils.cs, to try and reverse the order so that base class properties come first, but I've not had any success yet. Am I missing something trivial?

Upvotes: 13

Views: 3829

Answers (4)

Breno Santos
Breno Santos

Reputation: 110

Best way I found to the moment:

  • Do not use Converters
  • Stay cached by framework as usual
  • Use all default framework "types resolvers"
  • Still can use JsonOrder attribute

@Microsoft please fix PropertyInfo.DeclaryingType

...

services.AddJsonOptions(opts =>
{
    opts.JsonSerializerOptions.TypeInfoResolver = new CustomJsonTypeInfoResolver();
});

...

public class CustomJsonTypeInfoResolver : DefaultJsonTypeInfoResolver
{
    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        var baseTypeInfo = base.GetTypeInfo(type, options);

        if (baseTypeInfo != null && baseTypeInfo.Kind == JsonTypeInfoKind.Object)
        {
            var original = baseTypeInfo.Properties.ToList();

            var ordered = original
                .OrderBy(p => BaseTypesAndSelf(GetDeclaringType(p)).Count())
                .ToList();

            baseTypeInfo.Properties.Clear();

            foreach (var item in ordered)
            {
                baseTypeInfo.Properties.Add(item);
            }
        }

        return baseTypeInfo;
    }

    private static Type GetDeclaringType(JsonPropertyInfo propertyInfo)
    {
        var declaringType = propertyInfo.PropertyType.DeclaringType;

        //^^^^^ @MICROSOFT FIX THIS PLS ^^^^^^^^
        if (declaringType == null)
        {
            var declaringTypeProperty = typeof(JsonPropertyInfo).GetProperty(
                "DeclaringType",
                BindingFlags.NonPublic | BindingFlags.Instance
                );

            declaringType = declaringTypeProperty?.GetValue(propertyInfo) as Type;
        }

        return declaringType;
    }



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

Upvotes: 0

Jhonny D. Cano -Leftware-
Jhonny D. Cano -Leftware-

Reputation: 18013

Answer of @sinelaw does not work because property Order returns null unless you set up your properties with [JsonProperty(Order = <someInteger>)] which kinds of defeat the purpose of using a custom sort instead of using JsonProperty attributes.

I have modified its CustomPropertySortContractResolver to use default properties order when this order is not found.

public class CustomPropertySortContractResolver : DefaultContractResolver
{
    private const int MaxPropertiesPerContract = 1000;

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var members = GetSerializableMembers(type);
        if (members == null)
        {
            throw new JsonSerializationException("Null collection of serializable members returned.");
        }

        var propList = members
            .Select(member => CreateProperty(member, memberSerialization))
            .Where(x => x != null);

        var ind = 0;
        var orderedPropList = propList
            .OrderBy(p => ((p.Order != null ? p.Order : ind++) + (MaxPropertiesPerContract * GetTypeDepth(p.DeclaringType)) ?? -1))
            .ToList();

        return orderedPropList;
    }

    private static int GetTypeDepth(Type type)
    {
        int depth = 0;
        while ((type = type.BaseType) != null)
        {
            depth++;
        }

        return depth;
    }
}

Upvotes: 1

Moshtaf
Moshtaf

Reputation: 4903

Just to complement, I have suggested another approach different than accepted answer using [JsonProperty(Order = -2)] here:

Order of fields when serializing the derived class in JSON.NET

Upvotes: 4

sinelaw
sinelaw

Reputation: 16553

I don't think you need to change JSON.Net's code to do it. Apparently you can do it with a custom contract resolver - by inheriting from DefaultContractResolver - as shown in this code pasted by someone with a similar issue on the json.net forum. That poster overrides CreateProperties and sorts the properties by how deeply inherited the defining type is.

The following code is based on code from that post (by LittleColin on CodePlex). This compiles but isn't tested:

public class CustomPropertySortContractResolver : DefaultContractResolver
{
    private const int MaxPropertiesPerContract = 1000;

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var members = GetSerializableMembers(type);
        if (members == null)
        {
            throw new JsonSerializationException("Null collection of serializable members returned.");
        }

        return members.Select(member => CreateProperty(member, memberSerialization))
                      .Where(x => x != null)
                      .OrderBy(p => (p.Order
                                       + (MaxPropertiesPerContract * GetTypeDepth(p.DeclaringType))) 
                                    ?? -1)
                      .ToList();
    }

    private static int GetTypeDepth(Type type)
    {
        int depth = 0;
        while ((type = type.BaseType) != null)
        {
            depth++;
        }

        return depth;
    }
}

See also this project for similar code that filters the properties to be serialized.

Upvotes: 3

Related Questions