Home > Blockchain >  C# reflection - get object value from property value within JSON ContractResolver
C# reflection - get object value from property value within JSON ContractResolver

Time:05-13

I have a class PersonDto which contains a property instance of type AddressDto. I am building a custom ContractResolver named eg. ShouldSerializeContractResolver with Newtonsoft.Json marshalling .NET lib that will include only specific properties into serialization that are marked with my custom attribute eg. [ShouldSerialize]

The problem occurs when the CreateProperty method of the resolver goes into the complex / custom type of the PersonDto ie. it goes into the AddressDto and it is not aware that the property instance is tagged with the [ShouldSerialize] attribute. The resulting serialization then looks like "Address": {} instead of "Address": { "StreetNumber": 123 }

The code looks like:

class AddressDto 
{ 
  // PROBLEM 1/2: value does not get serialized, but I want it serialized as its property is [ShouldSerialize] attr tagged
  public int StreetNumber { get; set; } 
}

class PersonDto 
{ 
  public string Name { get; set; }  // should not serialize as has not attr on it

  [ShouldSerialize]
  public string Id { get; set; } 

  [ShouldSerialize]
  public AddressDto Address { get; set; }
}

// JSON contract resolver:

public class ShouldSerializeContractResolver: DefaultContractResolver
    {
        public ShouldSerializeContractResolver() { }

        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(member, memberSerialization);
            var attr = member.GetCustomAttribute<ShouldSerializeContractResolver>(inherit: false);

            // PROBLEM 2/2: here I need the code to access the member.DeclaringType instance somehow and then 
            // find its AddressDto property and its GetCustomAttribute<ShouldSerializeContractResolver>

            if (attr is null)
            {
                property.ShouldSerialize = instance => { return false; };
            }

            return property;
        }
    }

// code invoked as:

PersonDto somePerson = IrrelevantSomePersonCreateNewFactoryFn();

var jsonSettings = new JsonSerializerSettings { ContractResolver = new ShouldSerializeContractResolver() };
var strJson = JsonConvert.SerializeObject(somePerson, jsonSettings);

The serializer works in a "flat" mode, ie. it runs through all the props with the resolver and it comes to the point where the member is StreetNumber and from it I do not know how to access the "parent" MemberInfo, which would be great.

enter image description here

What I find as the core issue here is I do not have the "parent" / DeclaringType object instance and need to find a way on how to obtain it.

Please note that I can not solve this issue through [JsonProperty], [JsonIgnore] etc. as my attribute is complex and involves its own logic.

CodePudding user response:

You would like AddressDto to be serialized differently depending upon whether it was encountered via a property marked with [ShouldSerialize], however that cannot easily be done using a custom contract resolver because Json.NET creates exactly one contract for each type no matter where it is encountered in the serialization graph. I.e. a contract resolver will generate the same contract for AddressDto for both of the following data models:

class PersonDto 
{ 
    public string Name { get; set; }  // should not serialize as has not attr on it

    [ShouldSerialize]
    public string Id { get; set; } 

    [ShouldSerialize]
    public AddressDto Address { get; set; } // This and its properties should get serialized.
}

class SomeOtherDto
{
    [ShouldSerialize]
    public string SomeOtherValue { get; set; } 

    public AddressDto SecretAddress { get; set; }  // Should not get serialized.
}

This is why you cannot get the referring property's attributes when creating the properties for a referenced type.

Instead, you will need to track in runtime when the serializer begins and ends serialization of a [ShouldSerialize] property, setting some thread-safe state variable while inside. This can be done e.g. by using your contract resolver to inject a custom JsonConverter that sets the necessary state, disables itself temporarily to prevent recursive calls, then does a default serialization:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ShouldSerializeAttribute : System.Attribute
{
}

public class ShouldSerializeContractResolver: DefaultContractResolver
{
    static ThreadLocal<bool> inShouldSerialize = new (() => false);
    
    static bool InShouldSerialize { get => inShouldSerialize.Value; set => inShouldSerialize.Value = value; }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        var attr = member.GetCustomAttribute<ShouldSerializeAttribute>(inherit: false);

        if (attr is null)
        {
            var old = property.ShouldSerialize;
            property.ShouldSerialize = instance => InShouldSerialize && (old == null || old(instance));
        }
        else
        {
            var old = property.Converter;
            if (old == null)
                property.Converter = new InShouldSerializeConverter();
            else
                property.Converter = new InShouldSerializeConverterDecorator(old);
        }

        return property;
    }
    
    class InShouldSerializeConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var old = InShouldSerialize;
            try
            {
                InShouldSerialize = true;
                serializer.Serialize(writer, value);
            }
            finally
            {
                InShouldSerialize = old;
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();

        public override bool CanRead => false;
        public override bool CanConvert(Type objectType) => throw new NotImplementedException();
    }

    class InShouldSerializeConverterDecorator : JsonConverter
    {
        readonly JsonConverter innerConverter;
        
        public InShouldSerializeConverterDecorator(JsonConverter innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var old = InShouldSerialize;
            try
            {
                InShouldSerialize = true;
                if (innerConverter.CanWrite)
                    innerConverter.WriteJson(writer, value, serializer);
                else
                    serializer.Serialize(writer, value);
            }
            finally
            {
                InShouldSerialize = old;
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var old = InShouldSerialize;
            try
            {
                InShouldSerialize = true;
                if (innerConverter.CanRead)
                    return innerConverter.ReadJson(reader, objectType, existingValue, serializer);
                else
                    return serializer.Deserialize(reader, objectType);
            }
            finally
            {
                InShouldSerialize = old;
            }
        }

        public override bool CanConvert(Type objectType) => throw new NotImplementedException();
    }
}

Then serialize as follows:

IContractResolver resolver = new ShouldSerializeContractResolver(); // Cache statically & reuse for best performance

var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
};

var json = JsonConvert.SerializeObject(person, Formatting.Indented, settings);

Notes:

Demo fiddle here.

  • Related