0

I have a hierarchical class ConversationModel that needs to be serialized/deserialized to/from JSON.

Note that the ConversationNode class does NOT have a parameterless constructor. The only non-default constructor is also marked as internal. All class properties are get-only. This is deliberate to keep the class immutable, stateless, and idiot-proof.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;

public class ConversationModel
{
    public ConversationNode Node { get; }

    public ConversationModel ()
        => this.Node = new ConversationNode(this, null);

    public static JsonSerializerSettings JsonSerializerSettings
        => new JsonSerializerSettings
        {
            Formatting = Formatting.Indented,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            PreserveReferencesHandling = PreserveReferencesHandling.All,
            TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full,
            Converters = new [] { new ConversationModelJsonConverter(), },
        };

    public string Serialize ()
        => JsonConvert.SerializeObject(this, this.GetType(), ConversationModel.JsonSerializerSettings);

    public static ConversationModel FromJson (string json)
        => JsonConvert.DeserializeObject<ConversationModel>(json, ConversationModel.JsonSerializerSettings);
}

public class ConversationNode
{
    public ConversationModel Model { get; }
    public ConversationNode Parent { get; }
    public ConversationNodeList Nodes { get; }

    internal ConversationNode (ConversationModel model, ConversationNode parent)
    {
        this.Model = model ?? throw (new ArgumentNullException(nameof(model)));
        this.Parent = parent ?? throw (new ArgumentNullException(nameof(parent)));
        this.Nodes = new ConversationNodeList (model, parent);
    }
}

public class ConversationNodeList:
    List<ConversationNode>
{
    public ConversationModel Model { get; }
    public ConversationNode Parent { get; }

    public ConversationNodeList (ConversationModel model, ConversationNode parent)
    {
        this.Model = model;
        this.Parent = parent;
    }
}

The JsonSerializer, of course, expects a public, default, parameterless constructor. How can I get it to deserialize the model while keeping parent objects referenced properly as they are currently settable only through the constructor?

public sealed class ConversationModelJsonConverter:
    JsonConverter<ConversationModel>
{
    public override void WriteJson (JsonWriter writer, [AllowNull] ConversationModel value, JsonSerializer serializer)
    {
        // ???
    }

    public override ConversationModel ReadJson (JsonReader reader, Type objectType, [AllowNull] ConversationModel existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        // ???
        return (null);
    }
}

The question is:

  1. Is it possible to write a custom JsonConverter to achieve this? If so, how?
  2. Is there an easier way than having to implement a custom JsonConverter?
Raheel Khan
  • 14,205
  • 13
  • 80
  • 168
  • "The JsonSerializer, of course, expects a public, default, parameterless constructor" - that's not true. It can use non-public constructor, and it can use non-parameterless constructor also (like the constructor you currently have). – Evk Nov 15 '20 at 12:47
  • I marked the `internal` constructor with the `[JsonConstructor]` attribute and it throws a `JsonSerializationException` albeit somewhere down the hierarchy. In other words, it IS able to call the constructor but fails in some cases. The JSON being deserialized is generated using the same serializer with the same settings. Strange. – Raheel Khan Nov 15 '20 at 13:28
  • I think you need to provide reproducible example, because with current code it's possible to serialize and deserialize basic `new ConversationModel()` as is, without modifications (except changing `ConversationNode` to allow null parent). – Evk Nov 15 '20 at 13:30
  • `PreserveReferencesHandling` is documented to not work with a non-default constructor. See: [Newtonsoft.Json issue with deserialising relational model](https://stackoverflow.com/q/61205328/3744182) as well as the [docs](https://www.newtonsoft.com/json/help/html/PreserveObjectReferences.htm): *References cannot be preserved when a value is set via a non-default constructor. With a non-default constructor, child values must be created before the parent value so they can be passed into the constructor, making tracking reference impossible.* – dbc Nov 15 '20 at 16:02
  • That being said, Json.NET does in general support parameterized constructors. See: [How does JSON deserialization in C# work](https://stackoverflow.com/a/41871975/3744182) for the algorithm by which the appropriate constructor is chosen. – dbc Nov 15 '20 at 16:04
  • Rather than using the `PreserveReferencesHandling` to serialize and deserialize back references, you could omit the back references from the serialized JSON, and set them automatically as suggested in [How can I deserialize instances of a type that has read-only back-references to some container type also being deserialized?](https://stackoverflow.com/q/56316631/3744182). – dbc Nov 15 '20 at 18:12

0 Answers0