Newtonsoft for ISerializable classes with a reference loop use - use GetObjectData for serialization and ordinary constructor for deserialization

1 day ago 3
ARTICLE AD BOX

I have a couple of ISerializable classes (simplified):

internal interface IBase : ISerializable { int Number { get; } } internal interface ITable : IBase { ISection Section { get; } } [Serializable] internal class Table : ITable { public int Number { get; private init; } public ISection Section { get; internal set; } private Table() { } public Table(int number) { Number = number; } private Table(SerializationInfo info, StreamingContext context) { Number = info.GetInt32(nameof(Number)); Section = (ISection)info.GetValue(nameof(Section), typeof(ISection)); } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(nameof(Number), Number); info.AddValue(nameof(Section), Section); } } internal interface ISection : IBase { IReadOnlyList<ITable> Tables { get; } } [Serializable] internal class Section : ISection { public int Number { get; private init; } public IReadOnlyList<ITable> Tables { get; internal set; } private Section() { } public Section(int number) { Number = number; } private Section(SerializationInfo info, StreamingContext context) { Number = info.GetInt32(nameof(Number)); Tables = (IReadOnlyList<ITable>)info.GetValue(nameof(Tables), typeof(IReadOnlyList<ITable>)); } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(nameof(Number), Number); info.AddValue(nameof(Tables), Tables); } }

As you can see classes have private default constructor and private/internal properties setters.

And I create (with a reference loop) and serialize them the following way:

var table = new Table(1); var section = new Section(1); table.Section = section; section.Tables = new List<ITable> { table }; var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All, ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, ContractResolver = new CustomContractResolver() { IgnoreSerializableInterface = true } }; var json = JsonConvert.SerializeObject(section, settings); var restored = JsonConvert.DeserializeObject<Section>(json, settings);

In my CustomContractResolver, I gather all non-public members:

internal class CustomContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var prop = base.CreateProperty(member, memberSerialization); prop.Readable = true; if (!prop.Writable) { switch (member.MemberType) { case MemberTypes.Field: prop.Writable = true; break; case MemberTypes.Property: var hasPrivateSetter = (member as PropertyInfo)!.GetSetMethod(true) != null; prop.Writable = hasPrivateSetter; break; } } return prop; } protected override List<MemberInfo> GetSerializableMembers(Type objectType) { var members = base.GetSerializableMembers(objectType); var nonPublicFields = objectType.GetFields( BindingFlags.Instance | BindingFlags.NonPublic) .Where(f => !f.Name.EndsWith("k__BackingField")).ToList(); // excluding backing fields from auto-properties members.AddRange(nonPublicFields); var nonPublicProperties = objectType.GetProperties( BindingFlags.Instance | BindingFlags.NonPublic).ToList(); members.AddRange(nonPublicProperties); return members; } }

This works but in completely avoids both GetObjectData and constructor with SerializationInfo info, StreamingContext context. My goal is to keep using GetObjectData for serialization but use default constructor for deserialization (to restore references). I removed IgnoreSerializableInterface = true flag and added a custom converter:

internal class ISerializableConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(ISerializable).IsAssignableFrom(objectType); } public override bool CanRead => false; public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException("Deserialization is not used with this converter"); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var serializableObject = (ISerializable)value; var info = new SerializationInfo(value.GetType(), new FormatterConverter()); var context = new StreamingContext(StreamingContextStates.All); serializableObject.GetObjectData(info, context); writer.WriteStartObject(); foreach (SerializationEntry entry in info) { writer.WritePropertyName(entry.Name); serializer.Serialize(writer, entry.Value); } writer.WriteEndObject(); } } internal class CustomContractResolver : DefaultContractResolver { ... //I use ISerializableConverter here protected override JsonContract CreateContract(Type objectType) { var contract = base.CreateContract(objectType); if (contract is JsonISerializableContract) { //for deserialization we use constructor that was found by CreateObjectContract() var objectContract = CreateObjectContract(objectType); // setting converter for serilaizing via GetObjectData objectContract.Converter = new ISerializableConverter(); return objectContract; } return contract; } ... }

But with this approach I get an error:

Newtonsoft.Json.JsonSerializationException: 'Self referencing loop detected with type 'NewtonsoftJsonTest.Section'. Path 'Tables.$values[0]'.'

BTW I cannot add Newtonsoft attributes to my classes.

So is it possible to implement?

Read Entire Article