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.
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:
Newtonsoft recommends caching and reusing your contract resolver for best performance.
The above implementation has the limitation that, if your
[ShouldSerialize]
is also marked withJsonPropertyAttribute
, fields that control serialization of the property value such asItemConverterType
andIsReference
will be ignored.
Demo fiddle here.