4

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.

5
  • Easy exit to this is to mark the subtype AddressDto properties with [ShouldSerialize] but thats not what I want... Commented May 12, 2022 at 9:59
  • 1
    Wouldn't it be easier if you override CreateProperties instead and return the JsonProoerty as a colection from that call? You would still need to keep state within your resolver but it looks like you have the type you're looking for in that call signature. Not tested, just throwing ideas at you. Commented May 12, 2022 at 10:22
  • Uff hehe, thats a good one, to keep up with state, I honestly dont see another way at the moment, but this sorcery could work. Id need to keep a record on each of the runs (levels) and then when going deeper check out with the saved "parents". Commented May 12, 2022 at 10:36
  • 1
    You can't easily do what you want because Json.NET is a contract-based serializer that creates one contract for each type no matter where it is encountered in the serialization graph. You would like AddressDto to be serialized differently depending upon whether it was encountered via a property marked with [ShouldSerialize], but it's not designed to do that. You'll need to do something more complex like inject a converter that switches to a different serializer/contract resolver for each value marked with [ShouldSerialize]. Is it worth the extra effort? Commented May 12, 2022 at 10:59
  • That sounds hard for sure. But thank you for the idea. Atm sticking with explicit attr assignment to subtypes. I will possibly try and implement a state mechanism per @rene. Commented May 12, 2022 at 11:37

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

It does solve the original problem so I am marking it as answer, thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.